DLL Com GL Tutorial
DLL Com GL Tutorial
DLL Com GL Tutorial
You could do a
lot with a .DLL. If you had several programs that used the same functions or other resources, you could
save space by putting those resources in a .DLL. Putting code used by multiple programs in a single .DLL
often saved maintenance time because the code was all in one place. Fixes and other modifications would
only have to be done one time. If you had a program which needed to run different routines at different
times, you could put those routines into .DLLs and have the application load the appropriate .DLL when it
was needed. There were lots of good reasons to use .DLLs.
(continued)
WHITEPAPER :
CRM Done Right
Improve the likelihood of CRM success from less
than 20 percent to 60 percent.
WHITEPAPER :
CFO's Rising to the Challenge of
Performance Management
Summarizes the results of the IBM Business
Consulting Services CFO Survey.
SURVEY :
The 2005 Chief Procurement Officer Survey
Highlights several strategic imperatives that are
fundamentally altering the role of procurement.
There are still a lot of good reasons to use .DLLs. They haven't gone away. Sure, whatever you can do
with a .DLL, you can probably do with a COM object. Granted, there are a number of shortcomings to
.DLLs, some of them serious, which is why we ended up with COM in the first place. Still, .DLLs remain a
very useful tool. Compared to COM and ATL, they are much easier to make. Learning COM or ATL requires
a serious investment of time and effort. Making a .DLL is relatively easy. Modifying one is easy too. If you
know some C++ and MFC, you could be making .DLLs today.
This article will review the types of .DLLs you can make with MFC, including when to use each type and
how to make them. In the next article there will be a discussion of the limitations of .DLLs (which led to
the rise of COM and ATL), and how these can be partially avoided. In the third article, there will be more
coding details and examples.
Every .DLL has some kind of interface. The interface is the set of the variables, pointers, functions or
classes provided by the .DLL which you can access from the client program. They are the things that allow
the client program to use the .DLL. An MFC extension .DLL can have a C++ style interface. That is, it can
provide ("export") C++ functions and entire C++ classes to be used by the client application. The
functions it exports can use C++ or MFC data types as parameters or as return values. When it exports a
class, the client will be able to create objects of that class or derive new classes from it. Inside the .DLL,
you can also use MFC and C++.
The MFC code library used by Visual C++ is stored in a .DLL. An MFC extension .DLL dynamically links to
the MFC code library .DLL. The client application must also dynamically link to the MFC code library .DLL.
As the years have gone by the MFC library has grown. As a result, there are a few different versions of the
MFC code library .DLL out there. Both the client program and the extension .DLL must be built using the
same version of MFC. Therefore, for an MFC extension .DLL to work, both the extension .DLL and the
client program must dynamically link to the same MFC code library .DLL, and this .DLL must be available
on the computer where the application is running.
Note: If you have an application which is statically linked to MFC, and you wish to modify it so that it can
access functions from an extension .DLL, you can change the application to dynamically link to MFC. In
Visual C++, select "Project | Settings" from the menu. On the "General" settings tab you can change your
application to dynamically link to MFC.
MFC extension .DLLs are very small. You can build an extension .DLL which exports a few functions or
small classes and has a size of 10-15 KB. Obviously, the size of your .DLL depends on how much code you
store in it, but in general MFC extension .DLLs are relatively small and quick to load.
Regular .DLLs
The MFC extension .DLL only works with MFC client applications. If you need a .DLL that can be loaded
and run by a wider range of Win32 programs, you should use a regular .DLL. The downside is that your
.DLL and your client application cannot send each other pointers or references to MFC-derived classes and
objects. If you export a function, it cannot use MFC data types in its parameters or return values. If you
export a C++ class, it cannot be derived from MFC. You can still use MFC inside your .DLL, but not in
your interface.
Your regular .DLL still needs to have access to the code in the MFC code library .DLL. You can dynamically
link to this code or statically link. If you dynamically link, that means the MFC code your .DLL needs in
order to function is not built into your .DLL. Your .DLL will get the code it needs from the MFC code library
.DLL found on the client application's computer. If the right version of the MFC code library .DLL is not
there, your .DLL won't run. Like the MFC extension .DLL, you get a small .DLL (because the .DLL doesn't
include the MFC code), but you can only run if the client computer has the MFC code library .DLL.
If you statically link to the MFC code library, your .DLL will incorporate within itself all the MFC code it
needs. Thus, it will be a larger .DLL, but it won't be dependent on the client computer having the proper
MFC code library .DLL. If you can't rely on the host computer having the right version of MFC available,
this is the way to go. If your application users are all within your own company, and you have control over
what versions of the MFC .DLLs are lurking on their computers, or if your installation program also loads
the right MFC .DLL, this might not be an issue.
Building a .DLL
You can make an MFC-based .DLL with the App Wizard. Select "File | New" from the menu. On the
"Projects" tab, select "MFC AppWizard (.DLL)." Pick a name for your new project and click "OK." On the
next screen, you will have the choice to create an MFC extension .DLL, a regular .DLL "using shared MFC
.DLL" (i.e., a regular .DLL dynamically linked to MFC), or a regular .DLL statically linked to MFC. Pick the
one you want and click "Finish."
App Wizard builds a .DLL which doesn't do anything. The new .DLL will compile, but since it doesn't export
any classes or functions yet, it is still essentially useless. You now have two jobs: (1) add functionality to
make your .DLL useful; and (2) modify your client application to use your .DLL.
Export a class
Once you're done with the App Wizard, you can add classes to your .DLL by adding the .cpp and .h files
from another project, or you can create them from scratch within your current project. To export a class,
you add "__declspec(dllexport)" to the class declaration so it looks like this:
If you are making an MFC extension .DLL, you can instead use the AFX_EXT_CLASS macro:
There are other ways to export a class, but this is the easiest. If your exported class requires a resource
which is located in the .DLL, for example a class derived from CDialog, the process is more involved. I'll
cover this subject in tutorial #3. Below I'll discuss what to do to the client application so that it can use
your exported class.
Instead of exporting a whole class, you can have your .DLL export a variable, constant or object. To export
a variable or constant, you simply declare it like this:
When you want to export a constant, you must use the "extern" specifier. Otherwise you will get a link
error.
You can declare and export a class object in the exact same manner:
Note that you can only export a class object if the client application recognizes the class and has its
header file. If you make a new class inside your .DLL, the client application won't recognize it without the
header file.
When you export a variable or object, each client application which loads the .DLL will get its own copy.
Thus, if two different applications are using the same .DLL, changes made by one application will not
affect the other application.
It's important to remember that you can only export objects and variables which are of global scope
within your .DLL. Local objects and variables cease to exist when they go out of scope. Thus, if your .DLL
included the following, it wouldn't work.
MyFunction( )
{
__declspec(dllexport) CSomeClass SomeObject;
__declspec(dllexport) int SomeInt;
}
As soon as the object and variable go out of scope, they will cease to exist.
Export a function
Exporting functions is similar to exporting objects or variables. You simply tack "_declspec(dllexport)" onto
the beginning of your function prototype:
__declspec(dllexport) int SomeFunction(int);
If you are making an MFC regular .DLL which will be used by a client application written in C, your function
declaration should look like this:
If you are building a regular .DLL which is dynamically linked to the MFC code library .DLL, you must
insert the AFX_MANAGE_STATE macro as the first line of any exported function. Thus, your function
definition would look like this:
It doesn't hurt to do this in every regular .DLL. If you you switch your .DLL to static linking, the macro will
simply have no effect.
That's all there is to exporting functions. Remember, only an MFC extension .DLL can export functions with
MFC data types in the parameters or return value.
Export a pointer
Exporting an uninitialized pointer is simple. You do it the same way you export a variable or object:
Of course, if you declare and initialize your pointer you need to find a place to delete it.
In an extension .DLL, you will find a function called DllMain( ). This function gets called when the client
program attaches your .DLL and again when it detaches. So here's one possible way to handle your
pointers in an extension .DLL:
#include "SomeClass.h"
A regular .DLL looks more like an ordinary MFC executable. It has an object derived from CWinApp to
handle opening and closing your .DLL. You can use the class wizard to add an InitInstance( ) function and
an ExitInstance( ) function.
int CMyDllApp::ExitInstance()
{
delete SomePointer;
return CWinApp::ExitInstance();
}
When you compile your .DLL, the compiler creates two important files: the .DLL file and the .lib file. Your
client application needs both of these. You must copy them into the project folder of your client
application. Note that the .DLL and .lib files that are created when you build in Debug are different that
those built when you build in Release. When you are building your client application in Debug, you need
the Debug versions of the .DLL and .lib files, and when you are building in Release you need the Release
.DLL and .lib. The easiest way to handle this is to put the Debug .DLL and .lib files in your client
application's Debug folder and the Release .DLL and .lib in the Release folder.
The next step is to go into your client project settings and tell the linker to look for your .lib file. You must
tell the linker the name of your .lib file and where it can be found. To do this, open the project settings, go
to the "Link" tab and enter your file name and path in the "Object/library modules" box. It should look
something like this:
In addition to the .DLL and .lib files, your client application needs a header file for the imported classes,
functions, objects and variables. When we were exporting, we added "__declspec(dllexport)" to our
declarations. Now when we are importing, we will add "__declspec(dllimport)." So if we wanted to import
the variable, object and function used in our previous examples, our header file would contain the
following:
Remember, if you used the extern "C" specifier in the .DLL, you must also use it in the client application:
Now that you have declared your object, variable and function in a header file inside your client
application, they are available for use.
To import an entire class, you must copy the entire .h header file into the client application. The .DLL and
the client application will thus have identical header files for the exported class, except one will say "class
__declspec(dllexport) CMyClass" and one will say "class __declspec(dllimport) CMyClass". If you are
making an MFC extension .DLL, you could instead say "class AFX_EXT_CLASS CMyClass" in both places.
Once you are done building your client application and you're ready to turn it over to the actual users, you
should give them your Release executable and the Release .DLL. You do not need to give the users the .lib
file. The .DLL can go in the same directory as the executable, or it can go in the Windows System
directory. As discussed above, you may also have to provide your users with the correct MFC code library
.DLL. This .DLL was loaded onto your computer when you installed Visual C++. Your users, however, may
not have it. It does not come standard with Windows.
A Word of Caution
This article should provide you with enough information to start building your own .DLLs. A word of
caution is needed, however. As mentioned at the beginning of this article, there are several serious
shortcomings to .DLLs. These shortcomings are the reason that we now have COM and ATL. There are two
main problems. First, a .DLL built with one brand of compiler may not be compatible with a client
application built with a different compiler. Second, when you modify the .DLL, you may have to recompile
the client application, even though you aren't changing any code in the client application. You may still
have to copy in a new .DLL and .lib file and recompile.
There are ways to avoid this problem under some circumstances. I'll discuss the problem in more detail in
the next article.
Introduction
I have read a lot of good articles on CodeProject. Now it's time for me to provide one.
Please leave any comment or suggestion you may have. All are welcome and greatly
appreciated. I do want to get your feedback on this implementation of multiple language
support. Following requirements have been made for this implementation of multiple
language support for MFC applications with MFC extension DLL:
• Supporting one more language should not require changing the code any
more unless there is some kind of hard-code in the previous code. You should
remove all hard coding. E.g. all message strings should come from resources,
not hardcoded in CPP / H files.
• Supporting one more language only needs to be dealt with resource files -
*.rc and res/*.* files only. This means you can provide the resource file to
anyone, and they can build resource-only DLLs for a new language without
access to your code. Their new language DLL files should work with your
application.
• Each existing MFC EXE / DLL file has one resource-only language DLL file for
each language. Resource-only language DLL file is named after its application;
appended iwth three-letter language abbreviation codes. It uses DLL as suffix.
• Existing EXE / DLLs have the choice to store resources or not. If no other
language DLL exists, it searches the system default fake language “LOC”. If
that fails, it uses the resource stored in EXE / DLL file, if any.
• Resource-only language DLL files stay with MFC application in the same folder.
• Use Control Panel settings in user’s OS to determine which language DLL to
load to memory.
• Easily change to existing VC6 code to make it support the above features for
multiple languages. Change a few codes in InitInstance()/ExitInstance()
and DllMain() and that should be enough.
This document is for Visual Studio 6, may be version 5, but not for version 7, 7.1, 8.0.
MFC version 7 has some built-in support for multiple languages. This article includes the
following:
From VC7 and up, you shouldn't worry that much on multiple language support.
I wrapped some language related functions to a class. I made some changes for VC6, and
put a demo project using it. You need to exit and launch your application again for a new
language selected in the Control Panel.
Visual C++ allows you to choose different languages when you create an MFC
AppWizard program. The East Asian language support DLLs (Japanese, Korean,
and simplified Chinese) for the MFC AppWizard, which require double-byte
enabled operating systems, are not installed by default. Thus you will not see
them in the language drop-down list on the first page of the MFC AppWizard.
However, you can find them in the Microsoft Visual
Studio\Common\msdev98\bin\ide directory on the Visual C++ CD as follows:
– or–
If your application is statically linked to MFC, you must have the appropriate
localized MFC resource files in your MFC\[src|include]\L.XXX\*.rc directory.
You can find these files in the corresponding directories of Visual Studio CD1.
For more information on using the localized resources that Visual C++ provides,
see Tech Note 56 and Tech Note 57.
Static linking to MFC is supported only in Visual C++ Professional and Enterprise
Editions. For more information, see Visual C++ Editions.
Now build English resource only DLL files for Main.exe and subdll.dll. A resource-only
DLL is a DLL that contains nothing but resources, such as icons, bitmaps, strings, and
dialog boxes. It is also a good way to provide an application with resources localized for
multiple languages. See “Creating a Resource-Only DLL” (1.) in MSDN. Here are the
steps to build a resource-only language DLL. It's named after its EXE or DLL, appending
the three-letter language code. E.g. Main.exe has its English resource DLL as
MainENU.DLL. Following are the steps to build English (ENU) resource-only language
DLL from the previous German (DEU) code.
For MUI (Multiple User Interface) OS, a user can update his OS user language in Control
Panel, Region and language options, Language. Following code is partially copied from
VC7.1 appcore.cpp. It detects UI language, and stores a language list for loading later.
LANGID CMultiLanguage::DetectUILanguage()
{
LANGID langid = 0;
int nPrimaryLang = 0;
int nSubLang = 0;
LCID lcid = 0;
PFNGETUSERDEFAULTUILANGUAGE pfnGetUserDefaultUILanguage;
PFNGETSYSTEMDEFAULTUILANGUAGE pfnGetSystemDefaultUILanguage;
HINSTANCE hKernel32;
hKernel32 = ::GetModuleHandle(_T("kernel32.dll"));
ASSERT(hKernel32 != NULL);
pfnGetUserDefaultUILanguage =
(PFNGETUSERDEFAULTUILANGUAGE)::GetProcAddress(hKernel32,
"GetUserDefaultUILanguage");
if(pfnGetUserDefaultUILanguage != NULL)
{
// First, try the user's UI language
langid = pfnGetUserDefaultUILanguage();
AddLangId( langid );
TRACE(_T("CMultiLanguage::DetectUILanguage()"
_T" 1st/2nd = %04X\n"), langid );
langid = pfnGetSystemDefaultUILanguage();
AddLangId( langid );
TRACE(_T("CMultiLanguage::DetectUILanguage()"
_T" 3rd/4th = %04X\n"), langid );
}
else
{
// We're not on an MUI-capable system.
if (::GetVersion()&0x80000000)
{
// We're on Windows 9x, so look
// in the registry for the UI language
HKEY hKey = NULL;
LONG nResult = ::RegOpenKeyEx(HKEY_CURRENT_USER,
_T( "Control Panel\\Desktop\\ResourceLocale" ),
0, KEY_READ, &hKey);
if (nResult == ERROR_SUCCESS)
{
DWORD dwType;
TCHAR szValue[16];
ULONG nBytes = sizeof( szValue );
nResult = ::RegQueryValueEx(hKey, NULL, NULL, &dwType,
LPBYTE( szValue ), &nBytes );
if ((nResult == ERROR_SUCCESS) && (dwType == REG_SZ))
{
DWORD dwLangID;
int nFields = _stscanf( szValue, _T( "%x" ),
&dwLangID );
if( nFields == 1 )
{
langid = LANGID( dwLangID );
AddLangId( langid );
TRACE(_T("CMultiLanguage::DetectUILanguage()"
_T" 9X1st/2nd = %04X\n"), langid );
}
}
::RegCloseKey(hKey);
}
}
else
{
// We're on NT 4. The UI language
// is the same as the language of the
// version resource in ntdll.dll
HMODULE hNTDLL = ::GetModuleHandle( _T( "ntdll.dll" ) );
if (hNTDLL != NULL)
{
langid = 0;
::EnumResourceLanguages( hNTDLL, RT_VERSION,
MAKEINTRESOURCE( 1 ),
_AfxEnumResLangProc,
reinterpret_cast< LONG_PTR >( &langid ) );
if (langid != 0)
{
AddLangId( langid );
TRACE(_T("CMultiLanguage::DetectUILanguage()"
_T" NT1st/2nd = %04X\n"), langid );
}
}
}
}
if ( m_nLocales < MAX_NUM_LCID )
{
m_alcidSearch[m_nLocales] = LOCALE_SYSTEM_DEFAULT;
m_nLocales++;
} else {
m_alcidSearch[MAX_NUM_LCID-1] = LOCALE_SYSTEM_DEFAULT;
m_nLocales = MAX_NUM_LCID;
}
return LANGIDFROMLCID(m_alcidSearch[0]);
}
While the above code works for MUI OS, a user may change the default locale, not the
UI language. The following code detects the user default language, not the UI language.
LANGID CMultiLanguage::DetectLangID()
{
LANGID langid = 0;
int nPrimaryLang = 0;
int nSubLang = 0;
LCID lcid = 0;
int nLocales = 0;
return langid;
}
We can get user selected languages in the OS now. I tested these only on Win200 Pro and
WinXP Pro. If you can test this on 9X or NT, let me know if this failed or succeeded.
Like that in MFC version 7, we attempt to load the resource DLL for each of the
following languages in order, stopping when it finds one:
To detect the user’s default language and system default language, we should make a call
to DetectLangID(). For user and system’s UI language, calling DetectUILanguage()
will be OK. After these two calls, a list of languages requested is stored in the
CMultiLanguage::m_alcidSearch[] array. To load it, use the following:
//pszExtension = ::PathFindExtension(szModuleName);
//nNoExtension = pszExtension - szModuleName; temp. for ".exe"
nNoExtension = lstrlen(szModuleName) - 3 ;
// Quick and kind of dirty way to take ".exe"/".dll" away.
if ( langUpdateId != MAKELANGID(LANG_NEUTRAL,SUBLANG_NEUTRAL) )
{
alcid[nLocales] = MAKELCID(langUpdateId, SORT_DEFAULT);
nLocales++;
}
for ( int iLocale = 0; iLocale < m_nLocales; iLocale++ )
{
if ( m_alcidSearch[iLocale] != 0 )
{
alcid[nLocales] = m_alcidSearch[iLocale];
nLocales++;
}
}
for ( iLocale = 0; iLocale < nLocales; iLocale++ )
{
lcid = alcid[iLocale];
if (lcid == LOCALE_SYSTEM_DEFAULT)
lstrcpy(szLangCode, _T("LOC"));
else {
int nResult = ::GetLocaleInfo(lcid,
LOCALE_SABBREVLANGNAME, szLangCode, 4);
ASSERT( nResult == 4 );
if ( nResult == 0 )
return NULL;
}
return NULL;
}
hLangDLL = ::LoadLibrary(szResDLLName);
if(hLangDLL != NULL)
return hLangDLL;// Successful return
}
return hLangDLL;
}
We can call LoadLangResourceDLL(…) to load the first available language DLL in the
list. You can change the language ID detected from above. For example, you only include
the resource DLL for Chinese PRC, and you want to use it for Chinese Taiwan and
Chinese Singapore too. You can do this hard-coding here. If you don't want to hard code
here, you can make a duplicated copy of the resource-only DLL file with another three-
letter language name. (Copy XXXXCHS.DLL to XXXXCHT.DLL for this case.)
If your EXE or DLL files don't include resource, you need to have one system default
language "LOC" in your resource-only language DLL file list. This is the last chance the
system will try to find the language resource.
For Main.exe to load a language related resource-only DLL, add the following code to
main.cpp in the very beginning of CMain::InitInstance():
For the MFC extension DLL SubDLL.DLL to load language related resource-only DLL,
add the following code to SubDLL.cpp in DllMain(...) just before calling
CDynLinkLibrary(...):
You may want to free the library when system detaches the process.
Column one is LANGID; the lower 10 bits are for the language, the higher 6 bits are for the
sub-language. Column two is the three-letter language code. Column three is the three-
letter language code without sub-language. It looks for this language if the language in
column two does not exist. Column one and column four are got from “Language
Identifier”(3.); column two and column three are got from the GetLocaleInfo() function
on my XP Pro PC. See CMultiLanguage::PrintThreeLetterLanguageCodeList()
function in MultiLanguage.cpp for details.
Introduction
This article shows you how to create a Win32 and MFC DLL to dynamically link a
Library to your application. Microsoft Foundation Class (MFC) library can be used to
create simplified DLLs. The MFC supports two types of DLLs, regular and extension:
Win32 DLL (non-MFC Library based) in general, only supports regular DLLs. You
should use Win32 DLLs when your DLL is not using the MFC Library, Win32 is
substantially more efficient.
Extension DLLs are for developing re-useable binary code and it is for advance usage
(such as ATL and COM). DLLs are very useful, especially if you want to create programs
that are modular. You can go to the Microsoft MSDN web page if you want to know more
on DLLs.
This tutorial is in five parts. It gives a step by step procedure for developing a Win32
DLL and a regular MFC DLL object. There are three client applications, two Win32
console applications (one for Load Time linkage and the other illustrates Run Time
linkage), the third DLL client application uses the shared MFC Library.
Regular DLLs execute in the same memory space as the DLL client application, you don't
have to worry about marshaling data and pointers across process boundaries.
First, we are going to make the Win32 DLL core files for the project, W32DLL.xxx.
/*******************************************************
File name: DLLCode.h
Save and close this header file. Now we are going to create the DLL implementation file,
DLLCode.cpp.
Select New from the file menu, then select "C++ Source File" and name the file
DLLCode. Then click OK.
/*********************************************************
File name: DLLCode.cpp
void DLLfun1(char* a)
{
cout << a << endl;
};
DLLclass::DLLclass() {};
DLLclass::~DLLclass() {};
We want to access the DLL, so we need to export it. When the DLL is built, it also builds
something called an export library. The export library is similar to a regular library. It
contains all of the information necessary to dynamically link to the DLL at runtime.
When we export a function or a class, the function name and location is stored in the
export library. The application uses the DLL links in the export library to access the DLL.
To create the DLL export library, select "setting..." from the Project menu. Select the
C/C++ tab. Append, or insert, ",DLLDIR_EX" (without the quotation marks) to the
Preprocessor Definition text box. Then click OK. This will prevent compiler assumptions
and warnings.
Close the "Executable For Debug Session" dialog box, we ran the DLL prematurely.
Congratulation, you finished building the Win32 DLL and its export Library.
This is the simplest and most versatile DLL Client, without MFC. This flexibility is due
to the DLL export library information.
Now, we are going to make a Win32 console application, project DLLClient1. In this
application, the DLL is loaded at application startup, "Load Time" linkage.
1. Close any open workspace and files, then select New from the File menu.
2. Select Win32 Console Application.
3. In the Project name box, give the project a fitting name, say, DLLClient1,
then click Next.
4. Select "An empty project", then click Finish and OK.
Next, we are going to make the C/C++ source file: DLLClient1. Select New from the File
menu, then select "C/C++ Source File". In the File Name box, give it a fitting name, say,
DLLClient1, then press the Enter key.
/***********************************************************
File name: DLLClient1.cpp
***********************************************************/
#include <iostream>
#include <conio.h>
#include <windows.h>
#include "DLLCode.h"
#pragma comment(lib,"W32DLL.lib")
int main()
{
int a, b, c;
DLLclass classFromDLL;
classFromDLL.Arg = 6;
a = classFromDLL.Add(3, 2);
b = classFromDLL.Sub(3, 2);
c = classFromDLL.Arg;
cout << "DLL class Add function return: " << a << endl;
cout << "DLL class Sub function return: " << b << endl;
cout << "DLL class Arg Variable return: " << c << endl;
getch();
a = DLLArg;
b = DLLfun2(30);
cout << "\n\nDLL Variable DLLArg return: " << a << endl;
cout << "DLL function DLLfun2 return: " << b << endl;
getch();
return 0;
}
Save and close this C++ source file, then minimize the VC++ Studio window. We must
get some common files from the W32DLL project. So, Copy, don't move, the following
files to the DLLClient1 project directory:
• W32DLL\Debug\W32DLL.DLL
• W32DLL\Debug\W32DLL.lib
• W32DLL\DLLCode.h
If you don't see the .DLL file in the Debug or Release folders, then select the View, or
Tool (depending on the operating system) menu option, then select Folder Options. Next
click on the View tab. Select option: Show all files. Then re-examine the Debug or
Release folder.
Now maximize the VC++ Studio window, then click the "!" button to compile, build, and
run the application.
If you have a DLL you use in many projects, then you can take advantage of the VC++
file architecture. Copy the DLL file into the system directory and the LIB file into the
Visual C++ Lib directory. See Options on the Tools menu, and select the Directories tab,
there are search directories for EXEs, SOURCE CODEs, and LIBs.
In this part, the DLL is loaded at application startup time. The Operating System searches
the DLL in the following locations:
For simplicity, we just copy the common files to the target directory, DLLClient1.
This DLL Client is for DLLs which do not have an export library, hints, we can't import
the DLL header file. Note, we are restricted to function calls.
Now, we are going to make a Win32 console application, project DLLClient2.
In this application, the DLL is loaded during execution of the application, "Run Time"
DLL linkage.
1. Close any open workspace and files, then select New from the File menu.
2. Select Win32 Console Application.
3. In the Project Name box, give the project a fitting name, say, DLLClient2,
then click Next.
4. Select "An empty project", then click Finish and OK.
Select New from the File menu, then select "C/C++ Source File". In the File Name box,
give it a fitting name, say, DLLClient2, then press the Enter key.
/************************************************************
File name: DLLClient2.cpp
************************************************************/
#include <conio.h> // Header file containing getch() prototype
#include <iostream>
#include <windows.h>
MYFUN1 pfun1;
MYFUN2 pfun2;
HMODULE hMod; // handle to loaded library module
BOOL bRes; // BOOL to check if DLL was successfully
unloaded
cout << "DLL function DLLfun2 return: " << a << endl;
cout << "Press a key to exit" << endl;
getch();
///////////////////////////////////////////////////////
// This code will run if you compile the W32DLL project
// with the W32DLL.def file to explicitly export DLLArg.
int *i;
i = (int*) GetProcAddress(hMod, "DLLArg");
if (i)
{
cout << "Variable DLLArg is: " << *i << endl;
};
Save and close this C++ source file, then minimize the VC++ studio window. Copy, don't
move, the following file to the DLLClient2 project directory:
W32DLL\Debug\W32DLL.DLL
Now maximize the VC++ Studio window, then click the "!" button to compile, build, and
run the application.
In this part, the DLL is loaded at application run time. The LoadLibrary function is used
to load a DLL at run time. Being that we dynamically loaded the library, we should free
this resource when we are finished with it.
Addendum
The code in "Code snippet 4" was modified to add code for accessing the global variable
DLLArg. Note, in the application you just ran, DLLCient2, function call
GetProcAddress(hMod, "DLLArg") returned a NULL pointer. To add the global
variable name to the DLL object, you must add a new text file to the W32DLL project. Go
to New on the File menu. Select "Text File" on the File tag. Name the file:
"W32DLL.def", the "def" file extension is important. Click OK.
Save and recompile the W32DLL project. Copy the new W32DLL.DLL file to the project
DLLClient2 directory. Run the DLLClient2 application.
Note: You CANNOT export a class object to the DLL. The library object contains that
information, along with the header file.
This time, we are going to make the MFC DLL core files for the project, RMFCDLL.xxx.
Next, we are going to make the DLL declaration header file: DLLCode.h. Select New
from the File menu, then select "C/C++ Header File" and name the file DLLCode. Click
OK.
/******************************************************
File name: DLLCode.h
extern "C" {
// We delete the function DLLfun1 (as defined
// in the W32DLL DLL) it writes to
// a console, not a window, it isn't appropriate to
// call it from a MFC Client.
int DLLDIR DLLfun2(int);
void DLLDIR DrawEllipse ( CRect, CDC* ); // a MFC function call
};
Save and close this header file. Now we are going to create the DLLCode.cpp file. Select
New from the File menu, then select "C++ Source File". Name the file: DLLCode. Click
OK. Copy and paste the following code excerpt:
/************************************************************
File name: DLLCode.cpp
DLLclass::DLLclass() {};
DLLclass::~DLLclass() {};
Now, select "setting..." from the Project menu. Select the C/C++ tab. Append, or insert,
",DLLDIR_EX" (without the quotation marks) to the Preprocessor Definition text box.
Then click OK.
Click the "!" button to compile, build, and run the RDLLMFC project. Close the
"Executable For Debug Session" dialog box, we ran the DLL prematurely.
Congratulations, you finished building the Win32 DLL and its export Library.
This client application is a MFC application. It provides the framework, a window, for
the MFC CBrush object used in the DLL function DrawEllipse.
1. Close any open workspace and files, then select New from the File menu.
2. Select Project: MFC AppWizard(exe).
3. In the Project name box, give the project a fitting name, say, MFCAp, then
click OK.
4. Select the "Single document" radio button, then click Finish and OK.
Select "ClassWizard..." from the View menu. The Message Maps tab should be active. In
the "Class name:" dropdown box, select "CMFCApView". In the "Message functions:"
list box, double click the "OnDraw" function label.
You should see the View class OnDraw function code. Replace that stub code with the
following code excerpt, use copy and paste:
///////////////////////////////////////////////
// CMFCApView drawing
At the top of the file MCFApView.cpp, and somewhere after the line containing:
#include "stdafx.h", insert the following line:
#include "DLLCode.h"
Save and close the MFCApView.cpp file. Select "Setting..." from the Project menu. Select
the "Link" tab. In the "Object/Library modules:" text box, enter: "RDLLMFC.lib",
without the quotation marks. Then click OK.
Now, minimize the VC++ Studio window. Copy the following files to the MFCAp project
directory:
• RDLLMFC\Debug\RDLLMFC.DLL
• RDLLMFC\Debug\RDLLMFC.lib
• RDLLMFC\DLLCode.h
Now maximize the VC++ Studio window, then click the "!" button.
A note in closing, you shouldn't change the DLL interface, especially class objects which
are exported therein, because the v-table and class size are fitted at compile time. If you
change the DLL and other programs are using it, you should rename the new version; or
alternatively, you should re-compile all programs based on that DLL with the new
interface.
DCOM
Introduction
Welcome to this tutorial. In this series, I will strip the mystique, the headache, and
confusion from DCOM by giving you a comprehensive tutorial with a straight forward
example. OK, no promises - but I will give it a good try. In this series of articles, I will
show you how to use the following technologies:
These will be used to develop a sample client/server system to say "Hello, world from <
machine >" to the user! I haven't heard of anybody needing to do client/server
development in order to say, "Hello, world!", but that's what I'm doing.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with us as we go; this way, you can
learn while you code.
Environment
The tutorial's steps were completed by the author on a Windows NT 4, SP 5 network with
Visual C++ 6.0 SP 4. The author hasn't tested this on Windows 2000 because he doesn't
have a copy. If anything in a particular step of this tutorial doesn't work or works
differently under Windows 2000, please post a message to the message board. Pretty
much anything which works under Windows NT 4 SP 5 should work the same under
Windows 2000, because of backward-compatibility, unless Microsoft's been making
changes.
I have a motto: never write anything if it asks too much of the reader and not enough of
the writer. To this end, I am going to tell you straight away what this tutorial assumes that
you know. I would also include a 'Who Needs This Tutorial" section, much like they
include a "Who Needs to Read This Book," but I'll let you be the judge of whether or not
you need something.
I hope I'm not assuming too much of you. I'll guide you along doing this at the same level
or a little higher as how Microsoft guided us all through the Scribble tutorial. That is, I
won't hold your hand too much, but I won't confuse you either (at least, I don't think
Microsoft's explanations are confusing...).
I welcome any and all input to how I explain things; to yell at me or praise me (I can take
both), post on the message boards at the bottom of this step. Believe me, I will get
your post. This way, people can see both your question, and my answer.
Conventions Used
Since we'll be referring to class names, symbol names, and such, it's good to follow a
convention with this so that everything is consistent. Here's what we'll do:
Code Fragments
Code fragments will appear in their own yellowish blocks on the page. The lines of new
code that you'll have to add will appear in bold. If you don't have to add anything, but I'm
just pointing some code out for my health, then nothing will be in bold:
void CClass::Func()
{
// NOTE: Add this code
CAnotherClass class; // And this code
class.AnotherFunc(); // And this code
void CClass::LongFunc()
{
...
CClass class;
hResult = class.Func();
...
}
Variable Names
• m_szDisplayName
• CHelloWorld
• IHelloWorld
• DHelloWorldEvents instead of _IHelloWorldEvents
Functions
• Function()
• THIS_IS_A_SYMBOL
• ISayHello::SayHello()
• CServiceModule
• [IDL_attribute]
• __stdcall and typedef
• END_OBJECT_MAP()
Also, I would like to acknowledge the contributors to various articles and columns in
MSDN Magazine (formerly MSJ), whose work, reprinted in the MSDN Library, helped
me through the jungle of DCOM. This included columns by George Shepard and Don
Box, two very knowledgeable COM experts. Thanks also go to Charles Steinhardt, and
Tony Smith, two authors who have written for Visual C++ Developer.
The Approach
We will proceed step by step, following the methodology employed by Microsoft for
writing the explanation of the Scribble tutorial. In this tutorial, you will develop:
Before we plunge into writing any software, it's always good to design (just a little!) what
it's going to do! What we'll develop in this tutorial will work as shown in Figure 1 below:
Figure 1. The way things will work.
As you can see in Figure 1, our software will follow a simple pattern:
Yes, yes; I get the hint. We'll start by doing Step 1 of the tutorial in this article, and then
you can go on to the next step of the tutorial by simply clicking the Next button at the
bottom of this page.
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
When the steps above are done, we'll have a living, breathing, fully functional DCOM
application. At the bottom of the page for each step of this tutorial are Back and Next
links, which you can use to go to the previous or next step. There's also a link which takes
you to the 'Questions and Answers' page.
The Questions and Answers page gives me more space and the capability to display
screenshots when I post answers to readers' questions. Feel free to post your question to
The Code Project's message board at the bottom of each step. If there's a question that
gets asked often or whose answer is not so simple, I'll see it and put it on the Questions
and Answers page. This way, the answers can be helpful to all of the readers of The Code
Project. I'll then reply to the question on the message board and e-mail the author saying
"I answered this question on the Questions And Answers page!".
I really must stop flapping my gums so much. Some people like to talk to hear
themselves talk; I think that, maybe I like to write to read my own writing... But anyway,
enough. Let's plunge in with Step 1.
Figure 2. Selecting the ATL COM AppWizard in the New dialog box.
When the settings look the way you want them to, click OK. This brings up Step 1 of the
ATL COM AppWizard (which only has one step). This is shown in Figure 3, below, with
the 'Service' option button selected. This is OK; since the AppWizard doesn't let us set
anything else, the next thing to do is to click Finish:
Figure 3. Selecting that we want AppWizard to give us a service.
Why A Service?
A Windows Service is a great type of process to have as your ATL/COM server. Windows
Services, in my experience, have performed better as DCOM servers. Services can live
while the machine is not logged on, whereas most EXE programs may not run. Also,
Services are totally non-interactive, which is just fine when all you want your
components to do is to perform routine system tasks, such as reading files or running
programs, or providing monitoring services. You don't want windows popping up all
willy-nilly, say, on your server computer sitting in a room in Ireland when you are
running the client over in India. Plus, services are able to be started and stopped, and kept
track of, using the Control Panel.
See above as far as the stand-alone EXE is concerned. A DLL is not very utilitarian as a
remote server, because all DLLs *must* be mapped into the address space of an EXE
process on the server system. A special EXE process which maps DCOM server DLLs
into its address space, and then remotes the DLLs' component(s), is known as a
surrogate. DLLs and surrogates are altogether complicated beasts to maintain and
configure when you need remote access to your components. Especially since these are
not reference-counted or monitored by the system, in case the number of clients drops to
zero or if the surrogate hangs, leaving you dead in the water. So a Service is my favorite
choice.
Finishing Up
After you click Finish, the AppWizard displays the New Project Information dialog box,
shown in Figure 4. The New Project Information box only has a little bit of information
in it; it just tells us that AppWizard is going to give us the starting point of a brand-new
Windows NT Service which is also a DCOM server. However, this service doesn't have
any COM objects yet. We'll add those in Step 3.
Click OK to have AppWizard generate the HelloServ starter program. Now, we are
ready to go on to the next step, modifying the source files. We'll start with the modifying
in Step 2, which is the next step of this tutorial. Click the Next link below to proceed to
Step 2.
Introduction
Welcome to Step 2 of our DCOM tutorial. In this series, I will strip the mystique, the
headache, and confusion from DCOM by giving you a comprehensive tutorial with a
straightforward example. OK, no promises -- but I will give it a good try.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with us as we go; this way, you can
learn while you code. If you ever have problems along the way with this tutorial, feel free
to:
A diagram of how our software will eventually work is shown in Figure 1. The client
calls a method on the server, which then fires an event back to the client using a
connection point. This connection point's event sink is implemented in the client (using
MFC and ClassWizard!!!), and the client shows its user a message telling the user that the
server said "Hello!":
Figure 1. Diagram of our DCOM client/server set-up.
Remember, our steps in developing the software in this tutorial are as follows:
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
We're currently on Step 2 of the tutorial, where we modify some things in the starter
source code provided to us by the ATL COM AppWizard. We do this to:
To start, we need to provide Windows with a user-friendly name it can use to display this
service to the user. This is called the "Display Name" of the service. AppWizard already
set up and specified a "Service Name" for us: HelloServ. For the user's sake, let's come
up with something which is easier to understand: Hello World Server. To do this, we'll
add a m_szDisplayName member variable to the CServiceModule class, and a String
Table entry to hold the display name. We do things this way so that it's easy for us to
change the display name of the service to whatever we wish.
First, double-click on the CServiceModule icon in ClassView. This will jump you to
where the CServiceModule class is declared, in STDAFX.H. We need to add a
m_szDisplayName member variable to the class. Move the cursor to the // data
members section, and add the code shown below in bold:
// data members
public:
TCHAR m_szDisplayName[256]; // display name of service
TCHAR m_szServiceName[256];
...
};
Listing 1. Adding the m_szDisplayName member variable to the CServiceModule
class.
The next thing to do is to add an entry to the String Table, IDS_DISPLAY_NAME, to hold
the display name we want to use. Figure 2, shown below, illustrates adding the String
Table entry. To do this, complete these steps:
...
}
Listing 2. Adding a LoadString() call to CServiceModule::Init().
Now we have made sure that the IDS_DISPLAY_NAME string gets loaded into the
m_szDisplayName member variable of CServiceModule. The next thing to do is to
change the call to CreateService() in the CServiceModule::Install() function. The
call is shown in Listing 3 below in bold. The call is again shown in Listing 4, also
below, but this time the argument which you need to replace is shown in bold:
inline BOOL CServiceModule::Install()
{
...
...
}
Listing 3. The call to the Windows CreateService() function, as called by
AppWizard.
...
}
Listing 4. Changing the second m_szServiceName to m_szDisplayName.
Finally, the last part of Step 2 is to initialize everything properly. Go to the place in the
HELLOSERV.CPP file shown // PLACE THE CURSOR HERE comment in Listing 5:
#include "stdafx.h"
#include "resource.h"
#include < initguid.h >
#include "HelloServ.h"
#include "HelloServ_i.c"
CServiceModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
Add all of the following code at where I've just told you to place the cursor:
// Initialization successful
return TRUE;
}
Listing 6. Adding the InitApplication() function to the server. Next, we need to
replace some of the code in CServiceModule::Run() with a call to the
InitApplication() function which we added in Listing 6 above. Using ClassView,
go to the CServiceModule::Run() function, and delete the code shown in bold:
void CServiceModule::Run()
{
...
HRESULT hr = CoInitialize(NULL);
// If you are running on NT 4.0 or higher you can use the following
call
// instead to make the EXE free threaded.
// This means that calls come in on a random RPC thread
// HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
_ASSERTE(SUCCEEDED(hr));
...
}
Listing 7. Code to delete from the implementation of CServiceModule::Run().
Now replace what I told you to delete with the code shown in Listing 8 below. The code
to add is shown in bold:
void CServiceModule::Run()
{
...
HRESULT hr = S_OK;
if (!InitApplication())
return;
...
}
Listing 8. Code to add in order to replace what we've deleted.
We've now completed Step 2 of this tutorial. We added a display name to help the user,
and we fixed the security-initialization code for the service. Click Next to move on to the
next step of this tutorial, Step 3, click Back to go back to Step 1 of this tutorial, or click
the Questions and Answers button to jump to the Questions and Answers page! Good
luck.
Introduction
Welcome to Step 3 of our DCOM tutorial. In this series, I will strip the mystique, the
headache, and confusion from DCOM by giving you a comprehensive tutorial with a
straightforward example. OK, no promises -- but I will give it a good try.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with us as we go; this way, you can
learn while you code. If you ever have problems along the way with this tutorial, feel free
to:
Remember, our steps in developing the software in this tutorial are as follows:
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
We're currently on Step 3 of this tutorial, where we will use the ATL Object Wizard to
add a simple COM object, the HelloWorld object, to the server. This step will go by real
fast, so instead of my blathering any further, let's plunge in:
To add COM objects to ATL servers, we can either use the New ATL Object Wizard
provided by Visual C++, or we can add code by hand. I prefer to use the Wizards
provided by Visual C++ whenever I can, but then again, I'm lazy :)
Let's proceed. Open ClassView, and then right-click on the 'HelloServ classes' text at the
very top, and then click New ATL Object on the menu. The New ATL Object Wizard
appears, as shown in Figure 2, below. All we want for this tutorial is a simple COM
object, so click the Simple COM Object icon, and then click Next.
Figure 2. The New ATL Object Wizard with Simple COM Object selected.
After you've clicked Next, the ATL Object Wizard Properties dialog box should appear.
The cursor starts off with being in the Short Name text box. Type HelloWorld in this
box; as you type, the other fields are filled in automatically. Figure 3 below shows how
everything should look when you're done. Don't click OK yet, though!
Figure 3. The ATL Object Wizard Properties dialog with the Names tab filled in.
Now, click on the Attributes tab of the ATL Object Wizard Properties dialog box. Make
these selections:
When everything is set correctly, the Attributes tab should appear as that in Figure 4,
below. When everything's ready, click OK to have the New ATL Object Wizard generate
code for us and add our new COM object to the server.
Figure 4. The Attributes tab of the ATL Object Wizard Properties dialog when we're done
changing settings.
When everything's been added, ClassView should look like that shown in Figure 5. There
is still one more change we need to make, though. Notice that, in Figure 5, the name of
the event interface (for the connection point) is not _IHelloWorldEvents but instead
DHelloWorldEvents? This is the result of a little change I made to the code.
Figure 5. ClassView after adding the HelloWorld object and changing the name of the
event interface.
The DHelloWorldEvents name for our event interface is a loose convention of
Microsoft's which I'm following. DHelloWorldEvents is a dispinterface, so a D
belongs in front of its name. Make sense? Good. Double-click the _IHelloWorldEvents
icon in ClassView. This jumps you to the project's IDL source code, which declares all of
the OLE stuff that our project uses. Next, replace all instances of _IHelloWorldEvents,
highlighted in bold in Listing 1 below, and change them to DHelloWorldEvents,
highlighted in bold in Listing 2 below.
[
...
]
library HELLOSERVLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(...),
helpstring("_IHelloWorldEvents event interface")
]
dispinterface _IHelloWorldEvents
{
...
};
[
uuid(...),
helpstring("HelloWorld Class")
]
coclass HelloWorld
{
[default] interface IHelloWorld;
[default, source] dispinterface _IHelloWorldEvents;
};
};
Listing 1. The instances of the _IHelloWorldEvents interface, which we need to
replace with DHelloWorldEvents:
[
...
]
library HELLOSERVLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(...),
helpstring("DHelloWorldEvents event interface")
]
dispinterface DHelloWorldEvents
{
...
};
[
uuid(...),
helpstring("HelloWorld Class")
]
coclass HelloWorld
{
[default] interface IHelloWorld;
[default, source] dispinterface DHelloWorldEvents;
};
};
Listing 2. The _IHelloWorldEvents name replaced with DHelloWorldEvents.
That's it! We're finished with Step 3! See, that wasn't so bad, was it? Once you've made
the changes specified in the two Listings above to the IDL source code, click Save All on
the Visual C++ Toolbar. This should save changes to all your project files, and ClassView
should match Figure 5.
You're now ready to proceed to Step 4. To do so, either click the link preceeding this
sentence, or click Next below. You can find the source code from this step of the tutorial
available from the Download Link above. To go back to Step 2 of this tutorial, click Back
below.
Introduction
Welcome to Step 4 of our DCOM tutorial. In this series, I will strip the mystique, the
headache, and confusion from DCOM by giving you a comprehensive tutorial with a
straightforward example. OK, no promises -- but I will give it a good try.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with us as we go; this way, you can
learn while you code. If you ever have problems along the way with this tutorial, feel free
to:
Remember, our steps in developing the software in this tutorial are as follows:
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
We're currently on Step 4 of this tutorial, where we finally add working code to our
DCOM server. We'll add a method to the IHelloWorld interface, and we'll call this
method SayHello(). This method will get the network name of the host that it's
executing on, plus it will call a as-yet-unimplemented function Fire_OnSayHello(),
which in Step 5 we'll add as an event to our DHelloWorldEvents event interface. This
function will take a single [in] BSTR parameter, the name of the host. Anyway, enough
of my jabber; let's plunge in:
This step of the tutorial is really short. All we will do is add one method to our
ISayHello interface, and implement it using the CHelloWorld ATL class. Then we'll be
ready to move on to Step 5! Since the user of our client would like to have some
indication as to what computer on their network this code ran on, we'll add some code to
get the network name of that computer. The following listing, Listing 1, shows a piece of
code which you can cut-and-paste into any application you wish. This code calls the
Windows GetComputerName() function:
if (!GetComputerName(szComputerName, &dwSize))
{
// Display the cause of the error to the user with the
_com_error class
// To use the _com_error class, you need to #include <comdef.h>
in
// your STDAFX.H file
AfxMessageBox(_com_error(GetLastError()).ErrorMessage(),
MB_ICONSTOP);
return /*whatever error code: -1 or E_FAIL or whatnot here*/;
}
Let's now add the IHelloWorld::SayHello() method, and then add its code. To do this,
right-click the IHelloWorld interface icon in ClassView, and click Add Method. The Add
Method to Interface dialog box appears. Type SayHello in the Method Name box, and
leave the Return Type set to HRESULT.
TIP: When doing DCOM programming and adding methods to interfaces, *always*
set the Return Type of the method to HRESULT.
This allows DCOM to report network errors and other status to the client.
Anyway, getting back to what we're doing, when you're done filling in the Add Method to
Interface dialog box, it should look like that shown in Figure 1, below:
Click OK. When you do, the Add Method to Interface dialog will add code in all the right
places to make sure that when a call to the IHelloWorld::SayHello() method comes in
over the wire, the CHelloWorld::SayHello() member function will get called and
executed. After the method has been added, ClassView should resemble that shown in
Figure 2 below:
Figure 2. ClassView after the SayHello() method has been added.
Look at Figure 2. See the highlighted item? Double-click that item in your ClassView to
open up the CHelloWorld::SayHello() member function. This function implements the
IHelloWorld::SayHello() method. Let's add some code, shown here in bold, to
implement the method:
STDMETHODIMP CHelloWorld::SayHello()
{
// Get the network name of this computer
TCHAR szComputerName[MAX_COMPUTERNAME_LENGTH + 1];
DWORD dwSize = MAX_COMPUTERNAME_LENGTH + 1;
if (!GetComputerName(szComputerName, &dwSize))
return E_FAIL; // failed to get the name of this computer
return S_OK;
}
Listing 2. Adding code to implement the SayHello() method.
Notice the line which says // TODO: Add more code here? This tells us that
we're not done implementing yet; according to the design, this method should fire off
some sort of event back to the client. To see how to do this, click Next to advance to Step
5, where we finish the implementation of the server and get ready to move on to the
client. To go to the previous step, Step 3, click Back below. If you have questions, try
clicking Questions and Answers to go to a page which might help you.
Introduction
Welcome to Step 5 of our DCOM tutorial. In this series, I will strip the mystique, the
headache, and confusion from DCOM by giving you a comprehensive tutorial with a
straightforward example. OK, no promises -- but I will give it a good try.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with us as we go; this way, you can
learn while you code. If you ever have problems along the way with this tutorial, feel free
to:
Before we plunge in with Step 5 of our tutorial, let's just take a moment for me to rip the
shrouds of mystery off of Connection Points. Figure 1 below shows a generic scenario
which is true for COM, DCOM, and even function call backs, for goodness' sake.
Figure 1. A source and a sink.
This involves two objects, a "source" and a "sink". Think of the "source" like the water
faucet of the kitchen sink at home. You turn a handle, and stuff comes out of it (hopefully
water). Where does it go? If nothing's backed up, this water flows down into the bottom
and goes into the drain (which can be thought of as the "sink"). OK, so things flow from
the source, to the sink. In the kitchen sink analogy above, this is water. However, I've
never seen a computer network system run with water flowing across the wires, so
obviously something else is at work in DCOM.
In DCOM, there is a "client," somewhere on the network, and there is a "server," also
somewhere on the network. Without the use of connection points, things flow only one
way: method calls replace our water, the client replaces our faucet, and the server
replaces the drain. This is way oversimplifying things, but the user "turns a handle" (that
is, clicks a button, for example), and "stuff" (that is, method calls) "comes out of" the
client. This "stuff that comes out" then "flows" over the network using DCOM. These
calls "flow" to the server, which then collects them and acts like the "drain", or our sink.
Here's Figure 2, which is almost exactly like Figure 1, but puts the client in place of the
"source" and the server in place of the "sink," with the network in between:
Figure 2. Our client and server as the source and the sink.
OK, so now we have method calls flowing like water; wonderful. However, when the
client calls methods, the server does all kinds of things that might be interesting to
clients. So the server fires events all over the place. If our client doesn't care if the server
fires events, it will just ignore them. However, if it cares, it will Advise() the server.
Then the source-sink relationship of Figure 2 can be thought of in reverse:
As you can see, this is a round-trip. A method call goes from the client to the server, and
then an event call goes from the server, to the client, as seen in Figure 4.
Figure 4. A round-trip.
The Advise() step is done before item number 1 above, and the Unadvise() step (where
the client goes back to being aloof) happens after item number 4. The points of contact on
both the client and server and the Advise()ing and Unadvise()ing that happens all
together form...
A CONNECTION POINT!!
Whew... what a revelation... Let's start Step 5 before I get too carried away...
Let's plunge in, shall we? To add an event to the source, it's really, really easy. Just use
the Visual C++ Wizards! Open up ClassView, and right-click the DHelloWorldEvents
icon, and then click Add Method. The Add Method to Interface dialog box appears. Type
OnSayHello in the Method Name box, and type [in] BSTR bstrHost in the Parameters
box, as shown in Figure 5, below.
Once you're done, click OK. ClassView should resemble Figure 6 below. Now click
FileView, and find the HelloServ.idl file, under the Source Files folder. Right-click
that baby, and then choose Compile. Watch the compiler work away in the Output
window, and wait until the build is complete.
Figure 6. ClassView after adding the OnSayHello event.
Once the build has been finished, click on ClassView. Right-click the CHelloWorld class,
and then click Implement Connection Point. The Implement Connection Point dialog box
appears. If you haven't compiled the IDL file yet like I told you to, Visual C++ will
prompt you to do so. Figure 7, below, shows you how to select that you want to make the
server able to fire off its OnSayHello event:
When everything looks like Figure 7, click OK. The Visual C++ IDE will now generate
all the server-side code you need for Connection Points. Each time you change an event
in the DHelloWorldEvents event interface, you need to do the (1) compile the IDL, (2)
right-click CHelloWorld and choose Implement Connection Point, (3) check the box by
DHelloWorldEvents, and (4) click OK steps.
The final part of Step 5 which we have to take care of is firing the event. Remember, we
declared the OnSayHello() event in the IDL file as:
To fire the event from any CHelloWorld member function, just call Fire_OnSayHello().
It's a member function of a new base class, CProxyDHelloWorldEvents< > that the
Implement Connection Points dialog box just added for us. To this end, let's add code to
the CHelloWorld::SayHello() function to fire the event to the client:
STDMETHODIMP CHelloWorld::SayHello()
{
USES_CONVERSION;
if (!GetComputerName(szComputerName, &dwSize))
return E_FAIL; // failed to get the name of this computer
return S_OK;
}
Listing 2. Code to add to finish the CHelloWorld::SayHello() member function.
That's it! We're finished with Step 5. Click Next to go on to Step 6, or click Back to step
back to Step 4 if you're browsing through the tutorial. If you have any questions, try
clicking on Questions and Answers to go to the page with the good stuff, and then e-mail
me at [email protected] if you're still stuck.
Introduction
Welcome to Step 6 of our DCOM tutorial. In this series, I will strip the mystique, the
headache, and confusion from DCOM by giving you a comprehensive tutorial with a
straightforward example. OK, no promises -- but I will give it a good try.
If you want to follow along with this tutorial and add code and use the Visual C++
Wizards as we go along, that's great. In fact, I very very highly recommend that, because
otherwise this tutorial is a big waste of electronic ink (?). However, I follow along exactly
with the tutorial myself, as I write it, and develop the code and use the Visual C++
wizards just as I say you should. The screenshots, in fact, are from my development of
the files for each step! To download this already-developed code to compare with your
own, simply click the 'Download the Step n Files - n KB" links at the top of each step.
There's also an archive of the files for all the steps at the Questions and Answers page for
this tutorial. I still recommend that you follow along with me as we go; this way, you can
learn while you code. If you ever have problems along the way with this tutorial, feel free
to:
Remember, our steps in developing the software in this tutorial are as follows:
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by the AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
We're currently on Step 6 of this tutorial, where we build the server and also build and
register the Proxy-Stub DLL which goes along with it. Let's plunge in:
When you have reached this step, it's time to build our DCOM server, which is
implemented as a Windows NT Service. Before we click that Build button, there are
some things to do first. We start by making a few changes to the project settings, add a
Custom Build Step to the project in order to build and register our Proxy-Stub DLL, and
then we will make sure and change the configuration we're using. After doing all of this,
we will be ready to click the Build button.
To change the project settings, click the Project menu, and then click Settings. Click the
Custom Build tab; scroll over if you can't see it. Make sure you select the Win32 Release
MinDependency configuration in the Settings For drop-down. After you've done that,
erase everything in all of the fields of the Custom Build tab, so that what you have
matches Figure 1 below:
Figure 1. Removing the Custom Build step in the Project Settings dialog box.
Next, click the Post-Build Step tab. Again, before making changes, make sure you have
the Win32 Release MinDependency configuration selected in the Settings For
dropdown, as illustrated by Figure 2:
Figure 2. Making sure that Win32 Release MinDependency is selected.
Then type Building and registering Proxy-Stub DLL... in the Post-Build Description box,
and then type the following lines into the Post-Build Command(s) area, to match Figure
3:
The last thing to make sure to do before beginning the build is to make sure that the right
configuration is the active configuration. In our case, this is the Win32 Release
MinDependency configuration. Click Build on the menu bar, and then click Set Active
Configuration. This brings up the Set Active Configuration dialog box, as shown in
Figure 4. Click the HelloServ - Win32 Release MinDependency entry in the listbox, and
then click OK.
At last! Now we're ready to build. Click that good ol' Build button on the toolbar, and
watch the magic happen. When everything's done, you should have a HelloServ.exe EXE
file in the \ReleaseMinDepenedency subfolder of the project, and you should also see a
HelloServps.dll DLL in the main project directory. Copy those two files to a floppy disk,
and then put those in the C:\Winnt\System32\ directory on the computer you want to use
as the server. Make sure the server machine is running Windows NT 4.0 Workstation or
Server, or Windows 2000. Then, using the Run dialog box from the Start menu, run the
following command lines, in this order:
1. HelloServ /Service
2. regsvr32 HelloServps.dll
Now, we'll use your development machine (the one you're following this tutorial with) as
the client computer. To proceed, however, we'll need to follow these steps if the client
machine is running either Windows NT 4.0 or Windows 2000:
1. Copy the HelloServ.exe and HelloServps.dll files from the floppy disk to the
C:\Winnt\System32\ directory of the client machine.
2. Click the Start button, and then click Run.
3. Run the command line HelloServ /Service.
4. Click the Start button, and then click Run again.
5. Run the command line regsvr32 HelloServps.dll.
If your client machine is not running either Windows NT or Windows 2000, then you
have to follow these steps:
1. Make sure that the DCOM98 extensions, available here, are installed.
2. Copy the HelloServ.exe and HelloServps.dll files from the floppy disk to the
C:\Windows\System directory of the client machine.
3. Click the Start button, and then click Run.
4. Run the command line HelloServ /RegServer.
5. Click the Start button, and then click Run again.
6. Run the command line regsvr32 HelloServps.dll.
Now we are ready to proceed with Step 7. To move to Step 7, click Next below. If you
need to go back to Step 5, click Back. If you have questions or problems, try clicking
Questions and Answers below to jump to a page which offers some help.
•
Figure 1. Image of the HelloCli sample program.
Introduction
• Step 1: Create the server, HelloServ, using the ATL COM AppWizard.
• Step 2: Modify the starter files provided by AppWizard.
• Step 3: Use the New ATL Object Wizard to add a simple COM object, the
HelloWorld object, to the server.
• Step 4: Modify the IHelloWorld interface to include a SayHello() method.
• Step 5: Add an event method, OnSayHello(), to the connection point source
interface, DHelloWorldEvents.
• Step 6: Build the server, and install it on the server computer.
• Step 7: Create a MFC client, HelloCli, which calls the server and handles the
connection point event sink.
We're currently on Step 7 of this tutorial (the last!), where we put together a little MFC
program, called HelloCli, to test our server. Let's plunge in:
As you can see from the screenshot above, I built a dialog-based application using MFC
AppWizard (EXE). I added a status window to report status, so I can see just where errors
occurred, and I also successfully handled Connection Points. To add text to the status
window, which is just an Edit control with the read-only style set, I added a CString
member variable, m_strStatus to the dialog class with ClassWizard, and then anytime I
needed to add a message line to the edit control, it was made easy with this code:
We add on text to the contents of m_strStatus with the += operator of CString, and then
we call UpdateData(FALSE) to move the contents of the member variable from the
variable to the edit control.
To start my sample project, I brought up the New dialog box, clicked 'MFC AppWizard
(EXE)' in the list, and then typed 'HelloCli' for the name of my project. After
completing AppWizard, I opened the STDAFX.H file and added the line shown in bold in
Listing 2:
#if !
defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
#define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately
before
// the previous line.
#endif // !
defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
Listing 2. Adding the #define _WIN32_WINNT line to STDAFX.H so that DCOM works.
The next thing to do is to add something to let the client know about the interfaces and
everything else that the server supports. There are two ways we can do this:
• Using #import to bring in the type library of the server. This file,
HelloServ.tlb, is produced by MIDL when it compiles the HelloServ.idl
file.
• Using #include "HelloServ.h" to just include the C++ and C declarations of
interfaces. This is nice, but then you have to also define, in your code, all the
GUIDs that the server responds to. These are CLSID_HelloWorld,
IID_IHelloWorld, DIID_DHelloWorldEvents, and LIBID_HELLOSERVLib. If
you use #import, this is done for you.
I like the idea of using #import, because of not only what was explained above, but
also because you get to use smart pointers with #import, too. Be careful, though; we
have a custom (that is, IUnknown-derived) interface that our server uses. We can use
#import just fine in this example since the IHelloWorld::SayHello() method takes
no parameters. If the IHelloWorld::SayHello() method took parameters, and they
weren't of OLE-Automation-compatible types, then we would have to skip using
#import, because it will only recognize those types. However, if you mark your custom
interface with the [oleautomation] attribute and use OLE Automation-compatible types
in your methods, this will work.
With custom interfaces, it's generally a better idea to use the second method above.
However, like I said earlier, we'll go ahead and use #import this time because our
method doesn't take any parameters. So this means that we need to copy the
HelloServ.tlb file to our HelloCli project folder from the HelloServ project folder,
and then add an #import line somewhere. How about in good ol' STDAFX.H again?
We'll also add #include lines for atlbase.h and afxctl.h, since these files give us
support for things we'll use later on. Doing all this in STDAFX.H will help us when we
build our program to keep the build time down, too:
#if !
defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
#define AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately
before
// the previous line.
#endif // !
defined(AFX_STDAFX_H__8495B5E0_67FF_11D4_A358_00104B732442__INCLUDED_)
Listing 3. Adding other needed code to STDAFX.H.
NOTE: This only works if the event source interface is a dispinterface, like our
DHelloWorldEvents interface.
The next thing to do is to use ClassWizard to give us a class which will implement our
connection point for us (!). Yes, we've finally arrived!! To do this, open up ClassWizard,
click the Message Maps tab, click Add Class, and then click New, as shown below in
Figure 2:
Figure 2. Adding a new class with ClassWizard.
The next thing to do is to specify the new class we want to add to our project. Since this
class is (kind of) implementing an interface, that is, the DHelloWorldEvents
dispinterface, we'll call this class the CHelloWorldEvents class. Next, we specify that we
want to derive this class from the MFC CCmdTarget class, which helps us with all the
COM implementation. People have often said that "MFC doesn't really have any COM
support besides that needed for OLE and UI stuff. And then, it only does dispinterfaces."
None of the preceeding sentence is entirely correct. MFC is great at helping us out with
UI stuff, but I have seen example code (that works) where MFC is used to implement any
interface you like, even in COM servers with non-dispinterface and non-IDispatch
interfaces! The CCmdTarget class is the key.
Anyway, enough of my blathering. The last thing to do before we can click OK in the
New Class dialog box is to click the Automation option button. This turns on the support
in CCmdTarget that we need to use; don't worry, choosing this won't even add so much as
an .ODL file to your project, and you needn't have checked 'Automation' in AppWizard to
use this. When everything in the New Class dialog box is as it should be, it should look
like Figure 3, below:
Figure 3. Specifying the settings for our new CHelloWorldEvents class in ClassWizard.
ClassWizard will add the CHelloWorldEvents class to your project, but it will whine
because you didn't specify Automation support in AppWizard. Since you didn't, your
project doesn't have a HelloCli.odl file. Too bad for ClassWizard; it shows you the
protest message below, but you can click OK and ignore it:
Figure 4. ClassWizard should just grow up, and quit its whining; but, oh well... Ignore
this warning and click OK.
ClassWizard, helpful as it is, did make one booboo that we'll want to erase. Open the
HelloWorldEvents.cpp file and remove the line shown below in bold:
BEGIN_MESSAGE_MAP(CHelloWorldEvents, CCmdTarget)
//{{AFX_MSG_MAP(CHelloWorldEvents)
// NOTE - the ClassWizard will add and remove mapping macros
here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
BEGIN_DISPATCH_MAP(CHelloWorldEvents, CCmdTarget)
//{{AFX_DISPATCH_MAP(CHelloWorldEvents)
// NOTE - the ClassWizard will add and remove mapping macros
here.
//}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()
// {B0652FB5-6E0F-11D4-A35B-00104B732442}
static const IID IID_IHelloWorldEvents =
{ 0xb0652fb5, 0x6e0f, 0x11d4, { 0xa3, 0x5b, 0x0, 0x10, 0x4b, 0x73,
0x24, 0x42 } };
Listing 4. Delete the lines of code that are shown in bold.
Next, find the code shown in bold in Listing 5, below. We're going to replace it with the
DIID (DispInterfaceID) of the DHelloWorldEvents interface:
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget)
INTERFACE_PART(CHelloWorldEvents, IID_IHelloWorldEvents, Dispatch)
END_INTERFACE_MAP()
Listing 5. The code to look for, shown in bold.
BEGIN_INTERFACE_MAP(CHelloWorldEvents, CCmdTarget)
INTERFACE_PART(CHelloWorldEvents, DIID_DHelloWorldEvents,
Dispatch)
END_INTERFACE_MAP()
Listing 6. Putting DIID_DHelloWorldEvents in place of the Class-Wizard-added
IID_IHelloWorldEvents identifier.
Now, we're going to use ClassWizard to add the handler function which gets called when
the server fires the OnSayHello event. Bring up ClassWizard, and select the Automation
tab. Make sure that CHelloWorldEvents is selected in the Class Name box, as shown in
Figure 5 below:
Figure 5. Selecting the CHelloWorldEvents class on the Automation tab of
ClassWizard.
Click Add Method. The Add Method dialog box appears, as shown in Figure 6 below.
Here we're going to specify the "external name" for our event handler method, as well as
other information. The "external name" should ALWAYS match the name of the event
method that we used when we added it to the server! The Internal Name box should hold
the name of the member function that will get called when the event comes in; this can be
whatever you please. We're going to use what ClassWizard suggests; a name that matches
the OnSayHello "external name." ALWAYS specify void for the return type of an event
handler, because the server always uses HRESULT as its return types. For clients, anytime
we're handling connection point events, the return type should always be void. Next,
specify a parameter, LPCTSTR lpszHost as the event handler's single parameter. You
notice that BSTR isn't in the list of event handler types (alright!). This is because you use
LPCTSTR instead; ClassWizard makes sure that MFC will convert between BSTR and
LPCTSTR for you (!).
Figure 6. Setting up a handler for the OnSayHello event.
Click OK. ClassWizard adds code to CHelloWorldEvents to make the magic happen
(with CCmdTarget's help), and then shows a new entry in its External Names listbox to
show that the event handler has been added:
Figure 7. ClassWizard showing the addition of our new event handler.
Save your changes! Make sure and click OK in ClassWizard, otherwise it will roll-
back all of its changes that it made. If you click Cancel, you'll have to add the event
handler again. Just a word of warning. Now it's time to implement our event handler.
We'll grab the name of the server computer from lpszHost, and then we'll show the user
a message box saying that the server said Hello. We might also want to add text to the
Status window of our dialog saying that the event handler function got called. Here's how
I did that:
#include "HelloCliDlg.h"
void CHelloWorldEvents::OnSayHello(LPCTSTR lpszHost)
{
CHelloCliDlg* pDlg = (CHelloCliDlg*)AfxGetMainWnd();
if (pDlg != NULL)
{
pDlg->m_strStatus
+= "The OnSayHello() connection point method has been
called\r\n";
pDlg->UpdateData(FALSE);
}
// Show a message box saying 'Hello, world, from host ' + lpszHost:
CString strMessage = "Hello, world, from ";
strMessage += lpszHost;
AfxMessageBox(strMessage, MB_ICONINFORMATION);
}
Listing 7. Implementing the CHelloWorldEvents::OnSayHello() event handler
function.
...
};
The next thing to do is to add some data members to the CHelloCliDlg class in order to
hold the pointers and objects that we'll be using in working with the server. There are
quite a few of them, and you'll have to make sure to add the line
#include "HelloWorldEvents.h"
to the top of the HelloCliDlg.h file:
// Implementation
protected:
HICON m_hIcon;
m_dwCookie = 0;
m_pHelloWorldEventsUnk = m_events.GetIDispatch(FALSE);
// So we don't have to call Release()
m_bSinkAdvised = FALSE;
}
Listing 9. Code to add to the CHelloCliDlg::CHelloCliDlg() constructor function.
To advise the server, I added a AdviseEventSink(), protected member function to
CHelloCliDlg using ClassView. This function is implemented the basically the same for
anytime you want to advise a server about your MFC connection point:
BOOL CHelloCliDlg::AdviseEventSink()
{
if (m_bSinkAdvised)
return TRUE;
return bResult;
}
Listing 10. Implementation of advising the server. This demonstrates how to call
AfxConnectionAdvise(). You must have properly registered the server like we did in
Step 6, or else this won't work.
When we're ready to go back to being aloof to the server and its events that it fires, we
can call AfxConnectionUnadvise():
BOOL CHelloCliDlg::UnadviseEventSink()
{
if (!m_bSinkAdvised)
return TRUE;
if (spUnk.p)
{
// Unadvise the connection with the event source
return AfxConnectionUnadvise(pUnk, DIID_DHelloWorldEvents,
m_pHelloWorldEventsUnk, TRUE, m_dwCookie);
}
BOOL CHelloCliDlg::OnInitDialog()
{
CDialog::OnInitDialog();
...
CoInitialize(NULL);
return TRUE;
}
Listing 12. Adding intialization code to OnInitDialog().
The OnStartServer() function handles a button the user clicks when they want to start
the server (how circular). As you can see, I had a server computer on the network named
\\Viz-06 which I connected to with DCOM:
void CHelloCliDlg::OnStartServer()
{
COSERVERINFO serverInfo;
ZeroMemory(&serverInfo, sizeof(COSERVERINFO));
COAUTHINFO athn;
ZeroMemory(&athn, sizeof(COAUTHINFO));
serverInfo.pwszName = L"\\\\Viz-06";
serverInfo.pAuthInfo = &athn;
serverInfo.dwReserved1 = 0;
serverInfo.dwReserved2 = 0;
...
try
{
m_pHelloWorld = new IHelloWorldPtr;
}
catch(...)
{
AfxMessageBox(AFX_IDP_FAILED_MEMORY_ALLOC, MB_ICONSTOP);
...
return;
}
if (FAILED(hResult))
{
...
return;
}
m_pHelloWorld->Attach((IHelloWorld*)qi.pItf);
...
return;
}
Listing 13. How to get an interface pointer to the IHelloWorld interface on the
remote server.
To release the server when we're done with it, simply delete the m_pHelloWorld pointer:
delete m_pHelloWorld;
m_pHelloWorld = NULL;
Listing 15.
There! Now we have a living, breathing, DCOM client/server software system. It doesn't
do much, but it can do a lot... Anyway, I hope this tutorial has been enlightening, and
DCOM demystified. I always encourage you to e-mail me just whenever, and ask me
questions. No question is a stupid question, and I will be happy to help you. Click Back
below if you want to go back to Step 6, or click Questions and Answers to see if someone
else asked a question you need answered.
History
•
Introduction
Windows applications that do not export any program interface may be, however,
converted to an automation server. The term "Automation" in this context means that
clients running in separate processes are able to
This is useful particularly in the case of old legacy and the third party applications. Their
functionality and user interface may be upgraded, and their services become available for
another applications. This may be achieved with COM object(s) injected to the process.
The DLL injection technique in Win32 NT was described by Jeffrey Richter [1] and has
already become almost conventional. Now we try to move a step further: to embed a
COM object into a target application (now "promoted" to server) and, using this
component, communicate with the application from outside of its process.
• A special Loader application injects a Plugin DLL into the target process using
a remote thread. This is a worker thread, and its function is DllMain() of the
Plugin DLL. After DllMain() returns, the thread will be terminated.
• The Plugin DLL contains also callback window procedures to subclass the
frame and view windows of a target application. These window procedures
include message handlers with new functionality for the target application.
DllMain() of the Plugin DLL actually performs the subclassings.
• New window procedure for, say, a frame window has a handler for an
additional Plugin-specific Windows message, WM_CREATE_OBJECT. DllMain()
performs the target application window's subclassing, and after that posts the
WM_CREATE_OBJECT message to a frame window. Then DllMain() returns.
• Upon receiving the WM_CREATE_OBJECT message, a new frame window
procedure creates a COM object and calls its method for registration with
Running Object Table (ROT). If asynchronous notification to client(s) is
required then the embedded COM object should support the Connection Point
mechanism and define outgoing sink interface in its IDL file. It is preferable to
have the outgoing interface being an IDispatch-based (or dual) one to allow
a script client to implement it.
• Client applications obtain a proxy of the injected COM object via ROT, thus
gaining control over the target application. To subscribe for the server's
events notification, the clients should implement sink interface and marshal
("advise") its pointer to an embedded object.
To accomplish automation task information about the target application, the window class
name is required. It may be revealed by using Spy utility, which is part of the Visual
Studio installation. Another useful tool is ROT Viewer. It is also a Visual Studio utility.
ROT viewer allows developer to inspect the ROT and therefore check the proper
registration of a COM object embedded into the target process.
Test Sample
According to my scenario (just for illustration), the server (embedded COM object) picks
all Notepad editor keyboard input symbols (including non-alphabetic ones). As soon
as one of the end-of-the-sentence symbols (".", "?", and "!" characters) appears, the server
Fire_SentenceCompleted() event, thus providing all the event's subscribers with the
sentence completed (content of appropriate buffer).
Below two variants of clients, for Win32 and .NET platforms are discussed. Each of them
supports two ROT registration approaches: ActiveObject and moniker registration.
Registration details may be seen with the ROT Viewer tool.
Win32 Client
The client application process should implement the IHandlerEvents interface. It may
be done in various ways. I choose to create a special COM component (in-process
server), Notification. To construct it, the additional project NotepadHandlerNotification
was added to the workspace with the ATL COM AppWizard. Then an ATL object was
inserted into the component, using the right-click menu. This object implements
additional an interface that I called INotification. This interface is not a compulsory
one, but may be useful to provide the component with data from the client application via
appropriate methods (I supply to the component handle of the client application's main
window that way). The CNotification class implements IHandlerEvents interface (by
activating the Implement Interaface... right-click menu item followed by the
corresponding dialog). A client application class CAdvise contains advising mechanism.
The method CAutomationClientDlg::OnButtonAutomate() contains code responsible
for advising.
Implemented in the Notification component, the SentenceCompleted() method of
the IHandlerEvents interface is called by the server. The application main window
handle (in this case) is supplied to the Notification component with the
SetMainWnd() method of the INotification interface. Having this handle in its
possession, the SentenceCompleted() method posts a notification message to the
main window of the client application, passing a pointer to a buffer with data
received from the server.
.NET Client
An already compiled demo is available for the test sample. Please note that before you
run it, the NotepadHandler.dll and NotepadHandlerNotification.dll COM components
has to be registered with the regsvr32.exe utility. For the registration, you have to run
the file Register Components.bat, located in the demo directory. Then one or more copies
of AutomationClient.exe and AutomationClientNET.exe may be started.
By pressing the NOTEPAD AUTOMATE button, client internally starts the Loader.exe
application. The latter starts Notepad and automates it. A word [Automated] appears in
caption of Notepad main window. Alternatively, you may run Notepad manually before
the AutomationClient.exe. In this case, Loader automates the already running instance of
Notepad. As soon as Notepad has been automated client subscribes to its Sentence
Completed event.
Now you may type some text in the automated Notepad instance and press the Copy Text
button of the client application. The last text fragment you typed will appear in the edit
box of the client application. To simplify things, only actually typed characters and
symbols are copied, not copied-and-pasted text. Pressing the Find and Append Menu
buttons of the client causes corresponding Notepad response.
If user types some characters followed by a sentence conclusion sign (".", "?", or "!"), the
character sequence will be reproduced in an edit box of all clients' applications
subscribed to the event. The automated Notepad sends ("fires") the Sentence Completed
event to its subscribers (clients) as soon as any character from the set .?! is input. Upon
this event, the appropriate buffer content is sent to subscribers and displayed by them.
The test sample consists of NotepadAuto workspace for Visual C++ 6.0 and
AutomationClientNET solution for Visual Studio .NET (files NotepadAuto.dsw and
AutomationClientNET.sln are located in main directory of the source). VC++ 6.0 stuff is
completely independent from .NET solution and may be tested separately.
The AutomationClient.exe application should be run to test the Win32 sample. Please
note that sample should be tested outside Visual Studio. The AutomationClientNET.exe
application should be run to test the .NET sample. This test may be performed from
VS.NET too.
Conclusion
A technique for automation of Windows applications that do not export program interface
is presented. Usage of COM objects in a DLL injected into a target process allows
developer to automate such applications. This approach is useful to upgrade functionality
and GUI of target applications (particularly, legacy applications) and expose their
services to out-process clients. Source sample demonstrates such an automation for both
Win32 (MFC) and .NET (C#) clients.
References
[1] Jeffrey Richter. Advanced Windows. Third edition. Microsoft Press, 1997.
[2] John Peloquin. Remote Library Loading.
[3] Dino Esposito. Add Object Models to Non-COM Apps.
Update
The .NET client is added. ROT registration of injected COM object may be carried out
with ActiveObject or moniker. Text of the article is changed to reflect changes in code.
Thanks
My thanks to all people who read this article and expressed their opinion and suggestions.
My special thanks to Alex Furman for his refinement of RotHelper.Moniker class.
Igor Ladnik
•
Introduction
In the initial dialog, all the local folders of the main identity are listed to let the user
modify them.
In the message dialog, you will be able to view all the messages of the folder that you
selected in the folder dialog. Message source and other message operations can be done
here.
Points of Interest
List all local folders and add them to the list box:
hEnum = NULL;
HRESULT hr = m_pStoreNamespace->GetFirstSubFolder(dwFolderId,
&props, &hEnum);
hr = m_pStoreNamespace->GetNextSubFolder(hEnum, &props);
}
List all messages of a specific folder and add all 'Subject' and 'From' to the list box:
MESSAGEPROPS msgProps;
HENUMSTORE hEnumMsg;
CString item;
int nIndex;
hEnumMsg = NULL;
// as we want the subject and other staff we get all the properties.
// you can use MSGPROPS_FAST as first parameter
// to get only a few properties of the message.
HRESULT hr = m_pStoreFolder->GetFirstMessage(0,
0,
MESSAGEID_FIRST,
&msgProps,
&hEnumMsg);
hr = m_pStoreFolder->GetNextMessage(hEnumMsg, 0, &msgProps);
}
dwSelMsg = m_listMsg.GetItemData(nIndex);
CMsgSrcDlg msgSrcDlg;
if(FAILED(hr)) {
MessageBox(_T("Error reading message."), _T("Demo Error"));
}
else {
buffer[ulReaded] = 0;
msgSrcDlg.AddMessageSource(buffer);
}
} while(SUCCEEDED(hr) && ulReaded != 0);
if(SUCCEEDED(hr)) {
// display message
msgSrcDlg.DoModal();
}
pTextStream->Release();
}
// Commit the IStream in the folder and use the returned msgId
hr = m_pFolder->CommitStream(0, 0, 0, newMail, msgId, NULL);
if(FAILED(hr)) {
MessageBox(_T("Cannot Commit Stream."), _T("Demo Error"));
newMail->Release();
return;
}
Next
Introduction
Advanced COM-based projects often require the passing of objects across threads.
Besides the requirement to invoke the methods of these objects from various threads,
there is sometimes even the need to fire the events of these objects from more than one
thread. This two-part article is aimed at the beginner level COM developer who has just
crossed the initial hurdles of understanding the basics of IUnknown and IDispatch and is
now considering the use of objects in multiple threads. This is where the need to
understand COM Apartments come in.
I aim to explain in as much detail as possible the fundamental principles of how COM
object methods may be invoked from multiple threads. We shall explore COM
Apartments in general and the Single Threaded Apartment (STA) Model in particular
in an attempt to demystify both what they are designed to achieve and how they achieve
their design.
COM Apartments form a topic worthy of close study of its own. It is not possible to cover
in detail everything that pertains to this subject in one single article. Instead of doing that,
I will focus on Single-Threaded Apartments for now and will return to the other
Apartment Models in later articles. In fact, I have found quite a lot of ground to cover on
STAs alone and thus the need to split up this article into two parts.
This first part will concentrate on theory and understanding of the general architecture of
STAs. The second part will focus on solidifying the foundations built up in part one by
looking at more sophisticated examples.
I will present several illustrative test programs as well as a custom-developed C++ class
named CComThread which is a wrapper/manager for a Win32 thread that contains COM
objects or references to COM objects. CComThread also provides useful utilities that help
in inter-thread COM method calls.
I will show how to invoke an object's methods from across different threads. I will also
invoke an event of an object from another thread. Throughout this article, I will
concentrate my explanations on Single Threaded Apartment COM objects and threads
with some mention of other Apartment Models for comparison purposes. I chose to
expound on the STA because this is the Apartment Model most frequently recommended
by Wizards. The default model set by the ATL wizard is the STA. This model is useful in
ensuring thread-safety in objects without the need to implement a sophisticated thread-
synchronization infrastructure.
Synopsis
Listed below are the main sections of this article together with general outlines of each of
their contents:
COM Apartments
This section gives a general introduction to COM Apartments. We explore what they are,
what they are designed for, and why the need for them. We also discuss the relationship
between apartments, threads and COM objects and learn how threads and objects are
taught to live with each other in apartments.
This section begins our in-depth study of the Single-Threaded Apartments and serves as a
"warm-up" to the heavy-going sections that follow. We layout clearly the thread access
rules of an STA. We also see how COM makes such effective use of the good old
message loop. We then touch on the advantages and disadvantages of STAs in general
before proceeding to discuss implementation issues behind the development of STA
COM objects and STA threads.
This section and the next ("EXE COM Servers And Apartments") are filled with detailed
descriptions of several test programs. This is the main aim of this article: to show
concepts by clear examples. In this section, each test program is aimed at demonstrating
one particular type of STA (beginners may be surprised to learn that there are actually
three types of STAs !). The reader will note that our approach to demonstrating STAs is
very simple. The challenge for me is to demonstrate clearly the different types of STAs
using this simple test principle.
The last major section of this article explores EXE COM Servers and their relationship
with Apartments. Some of the important differences between a DLL Server and an EXE
Server are listed. From this section, I hope the reader gets to understand the important
role that Class Factories play. I have deliberately written by hand the source codes used
for the demonstration program in order to illustrate some concepts. The use of ATL
Wizards will have made this more troublesome.
Without further ado, let us begin by exploring the principles behind the COM Apartments
in general.
COM Apartments
To understand how COM deals with threads, we need to understand the concept of an
apartment. An apartment is a logical container inside an application for COM objects
which share the same thread access rules (i.e., regulations governing how the methods
and properties of an object are invoked from threads within and without the apartment in
which the object belongs).
It is conceptual in nature and does not present itself as an object with properties or
methods. There is no handle type that can be used to reference it nor are there APIs that
can be called to manage it in any way.
This is perhaps one of the most important reasons why it is so difficult for newbies to
understand COM Apartments. It is so abstract in nature.
Apartments may have been much easier to understand and learn if there was an API
named CoCreateApartment() (with a parameter that indicates the apartment type), and
some other supporting APIs like CoEnterApartment(). It would have been even better
still if there was a Microsoft supplied coclass with an interface like IApartment with
methods that manage the threads and objects inside an apartment. Programmatically,
there seem to be no tangible way to look at apartments.
To help the newbie cope with the initial learning curve, I have the following advise on the
way to perceive apartments:
1. They are created by implication. There are no direct function calls to create
them or to detect their presence.
2. Threads and objects enter apartments and engage in apartment-related
activities also by implication. There are also no direct function calls to do so.
3. Apartment Models are more like protocols, or a set of rules to follow.
It is towards resolving this issue that COM Apartments are created. COM Apartments
exist for the purpose of ensuring something known as thread-safety. By this, we mean the
safe-guarding of the internal state of objects from uncontrolled modification via equally
uncontrolled access of the objects' public properties and methods running from different
threads.
There are three types of Apartment Models in the COM world: Single-Threaded
Apartment (STA), Multi-Threaded Apartment (MTA), and Neutral Apartment. Each
apartment represents one mechanism whereby an object's internal state may be
synchronized across multiple threads.
Apartments stipulate the following general guidelines for participating threads and
objects:
• Each COM object is assigned to live in one and only one apartment. This is
decided at the time the object is created at runtime. After this initial setup,
the object remains in that apartment throughout its lifetime.
• A COM thread (i.e., a thread in which COM objects are created or COM
method calls are made) also belongs to an apartment. Like COM objects, the
apartment in which a thread lives is also decided at initialization time. Each
COM thread also remains in their designated apartment until it terminates.
• Threads and objects which belong to the same apartment are said to follow
the same thread access rules. Method calls which are made inside the same
apartment are performed directly without any assistance from COM.
• Threads and objects from different apartments are said to play by different
thread access rules. Method calls made across apartments are achieved via
marshalling. This requires the use of proxies and stubs.
From here onwards until the section "EXE COM Servers And Apartments" later on
below, we will refer to COM objects which are implemented in DLL servers.
As mentioned, a COM object will belong to exactly one runtime apartment and this is
decided at the time the object is created by the client. However, how does a COM object
indicate its Apartment Model in the first place?
Well, for a COM coclass implemented in a DLL Server, when COM proceeds to
instantiate it, it refers to the registry string value named "ThreadingModel" which is
located in the component's "InProcServer32" registry entry.
This setting is controlled by the developers of the COM object themselves. When you
develop a COM object using ATL, for example, you can specify to the ATL Wizard the
threading model the object is to use at runtime.
The table below shows the appropriate string values and the corresponding Apartment
Model that each indicates:
Now, onto threads. Every COM thread must initialize itself by calling the API
CoInitializeEx() and passing as the second parameter either
COINIT_APARTMENTTHREADED or COINIT_MULTITHREADED.
A thread which has called CoInitializeEx() is a COM thread and is said to have
entered an apartment. This will be so until the thread calls CoUninitialize() or simply
terminates.
An STA can contain exactly one thread (hence the term single-threaded). However, an
STA can contain as many objects as it likes. The special thing about the thread contained
within an STA is that it must, if the objects are to be exported to other threads, have a
message loop. We will return to the subject of message loops in a sub-section later on and
explore how they are used by STAs.
A COM object enters an STA both by specifying "Apartment" in the appropriate string
value in the registry and by being instantiated inside an STA thread.
In the above diagram, we have two apartments. Each apartment contains two objects and
one thread. We can postulate that each thread has, early in their life, called
CoInitialize(NULL) or CoInitializeEx(NULL, COINIT_APARTMENTTHREADED).
We can also tell that Obj1, Obj2, Obj3 and Obj4 are each marked as of "Apartment"
threading model in the registry, and that Obj1 and Obj2 were created inside Thread1 and
Obj3 and Obj4 were created inside Thread2.
1. An STA object created inside an STA thread will reside in the same STA as its
thread.
2. All objects inside an STA will receive method calls only from the thread of the
STA.
Point 1 is natural and is easily understood. However, note that two objects of the same
coclass and from the same DLL server created in separate STA threads will not be in the
same apartment. This is illustrated in the diagram below:
Hence any method calls between Obj1 and Obj2 are considered cross-apartment and must
be performed with COM marshalling.
Concerning point 2, there are only two ways that an STA object's methods are invoked:
1. From its own STA thread. In this case, the method call is naturally serialized.
2. From another thread (whatever the Apartment). In this case, COM ensures
that the object will receive method calls only from its own STA thread by
stipulating that this STA thread must contain a message loop.
We have mentioned this point about message loops previously, and before we can go on
discussing the internals of STAs, we must cover the subject of message loops and see
how they are intimately connected with STAs. This is discussed next.
The Message Loop
A thread that contains a message loop is also known as a user-interface thread. A user-
interface thread is associated with one or more windows which are created in that thread.
The thread is often said to own these windows. The window procedure for a window is
called only by the thread that owns the window. This happens when the
DispatchMessage() API is called inside the thread.
Any thread may send or post a message to any window but the window procedure of the
target window will only be executed by the owning thread. The end result is that all
messages to a target window are synchronized. That is, the window is guaranteed to
receive and process messages in the order in which the messages are sent/posted.
The benefit to Windows application developers is that window procedures need not be
thread-safe. Each window message becomes an atomic action request which will be
processed completely before the next message is entertained.
This presents to COM a readily available, built-in facility in Windows that can be used to
achieve thread-safety for COM objects. Simply put, all method calls from external
apartments to an STA object are accomplished by COM posting private messages to a
hidden window associated with that object. The window procedure of that hidden
window then arranges the call to the object and arranges the return value back to the
caller of the method.
Note that when external apartments are involved, COM will always arrange for proxies
and stubs to be involved as well so message loops form only part of the STA protocol.
Take note that in some circumstances, an STA thread need not contain a message loop.
We will return to explain this in the section "Implementing an STA Thread" later on.
It should be clear now how an STA achieves its thread access rules.
The main advantage to using an STA is simplicity. Besides a few basic code overheads
for COM object servers, relatively few synchronization code is necessary for the
participating COM objects and threads. All method calls are automatically serialized.
This is especially useful for user-interface-based COM objects (a.k.a. COM ActiveX
Controls).
Because STA objects are always accessed from the same thread, it is said to have thread
affinity. And with thread affinity, STA object developers can use thread local storage to
keep track of an object's internal data. Visual Basic and MFC use this technique for
development of COM objects and hence are STA objects.
Besides using it for benefits, it is sometimes inevitable to use STAs when there is a need
to support legacy COM components. COM components developed in the days of
Microsoft Windows NT 3.51 and Microsoft Windows 95 could only use the Single-
Threaded Apartment. Multi-Threaded Apartments became available for usage in
Windows NT 4.0 onwards and in Windows 95 with DCOM extensions.
There is a flip side to everything in life and there are disadvantages to using STA. The
STA architecture can impose significant performance penalties when an object is
accessed by many threads. Each thread's access to the object is serialized and so each
thread must wait in line for its turn to have a go with the object. This waiting time may
result in poor application response or performance.
The other issue which can result in poor performance is when an STA contains many
objects. Remember that an STA contains only one thread and hence will contain only one
thread message queue. This being the case, calls to separate objects within that STA will
all be serialized by the message queue. Whenever a method call is made on an STA
object, the STA thread may be busy servicing another object.
The disadvantages of using the STA must be measured against the possible advantages. It
all depends on the architecture and design of the project at hand.
Implementing an STA COM object generally frees the developer from having to serialize
access to the object's internal member data. However, the STA cannot ensure the thread-
safety of a COM server DLL's global data and global exported functions like
DllGetClassObject() and DllCanUnloadNow(). Remember that a COM server's
objects could be created in any thread and that two STA objects from the same DLL
server can be created in two separate STA threads.
In this situation, the global data and functions of the server may well be accessed from
two different threads without any serialization from COM. The message loops of the
threads cannot lend any help either. After all, it is not an object's internal state that is at
stake here. It is the server's internal state. Hence all access to global variables and
functions of the server will need to be serialized properly because more than one object
may try to access these from different threads. This rule also applies to class static
variables and functions.
One well-known global variable of COM servers is the global object count. This
variable is accessed by the equally well-known global exported functions
DllGetClassObject() and DllCanUnloadNow(). The APIs InterlockedIncrement()
and InterlockedDecrement() may be used to protect simultaneous access (from
different threads) to the global object count. DllGetClassObject() will in turn make
use of the class factories of COM objects and these must be examined for thread-safety
too.
Hence the following is a general guideline for implementing STA Server DLLs:
1. Server DLLs must have thread-safe standard entry point functions (e.g.,
DllGetClassObject() and DllCanUnloadNow()).
2. Private (non-exported) global functions of the Server DLL must be thread-
safe.
3. Private global variables (especially the global object count) must be thread-
safe.
The DllCanUnloadNow() function returns a value to its caller that determines whether
the COM Server DLL contains objects which are still alive and are servicing clients. This
DllCanUnloadNow() function uses the global object count to decide its return value. If no
more objects are still alive, the caller can safely unload the COM Server DLL from
memory.
CSomeObject::CSomeObject()
{
// Increment the global count of objects.
InterlockedIncrement(&g_lObjsInUse);
}
CSomeObject::~CSomeObject()
{
// Decrement the global count of objects.
InterlockedDecrement(&g_lObjsInUse);
}
The above code snippets show how the global object counter "g_lObjsInUse" is
incremented using the InterlockedIncrement() API during the constructor of an object
implemented by the C++ class CSomeObject. Conversely, during the destructor of
CSomeObject, "g_lObjsInUse" is decremented by the InterlockedDecrement() API.
No details can be advised on how to ensure the thread-safety of private global functions
and global variables. This must be left to the expertise and experience of the developers
themselves.
Ensuring thread-safety for a COM server need not be a complicated process. In many
situations, it requires simple common sense. It is safe to say that the above guidelines are
relatively easy to comply with and do not require constant re-coding once put in place.
Developers using ATL to develop COM servers will have these covered for them (except
for the thread-safety of private global data and functions) so that they can concentrate
fully on the business logic of their COM objects.
The code snippet above looks vaguely similar to a WinMain() function. In fact, the
WinMain() of a Windows application runs in a thread too.
In fact, you can implement your STA thread just like a typical WinMain() function. That
is, you can create windows just prior to the message loop and run your windows via
appropriate window procedures. You may opt to create COM objects and manage them in
these window procedures. Your window procedures may also make cross-apartment
method calls to external STA objects.
However, if you do not intend to create windows inside your thread, you will still be able
to create, run objects and make cross-apartment method calls across external threads.
These will be explained when we discuss some of the advanced example codes in part
two of this article.
Take note that in some cases, a message loop is not required in an STA thread. An
example of this can be seen in simple cases where an application simply creates and uses
objects without having its objects marshaled to other apartments. The following is an
example:
int main()
{
::CoInitialize(NULL);
if (1)
{
ISimpleCOMObject1Ptr spISimpleCOMObject1;
spISimpleCOMObject1.CreateInstance(__uuidof(SimpleCOMObject1));
spISimpleCOMObject1 -> Initialize();
spISimpleCOMObject1 -> Uninitialize();
}
::CoUninitialize();
return 0;
}
The above example shows the main thread of a console application in which an STA is
established when we call CoInitialize(). Note that there is no message loop defined
inside this thread. We also go on to create a COM object based on the
ISimpleCOMObject1 interface. Note that our calls to Initialize() and
Uninitialize() go successfully. This is because the method calls are made inside the
same STA and no marshalling and no message loop is required.
1. The calls to Initialize() and Uninitialize() will be made with the help of
COM marshalling.
2. The COM object spISimpleCOMObject1 will reside in a default STA created by
the COM sub-system.
3. The main() thread still does not need any message loop, but...
4. A message loop will be used in the calls to Initialize() and
Uninitialize().
The message loop that is used in this context is the message loop that is defined in the
default STA. We will talk about the default STA later on in the section on "The Default
STA".
Note that whenever you do need to provide a message loop for an STA thread, then you
must ensure that this message loop is serviced constantly without disruption.
We will now attempt to demonstrate STAs. The approach we use is to observe the ID of
the thread which is executing when a COM object's method is invoked. For a standard
STA object, this ID must match that of the thread of the STA.
If an STA object does not reside in the thread in which it is created (i.e., this thread is not
an STA thread), then the ID of this thread will not match that of the thread which
executes the object's methods. This basic principle is used throughout the examples of
this article.
Let us now observe STAs in action. To start, we examine the standard STA. A process
may contain as many standard STAs as is required. Our example uses a simple example
STA COM object (coclass SimpleCOMObject2 which implements interface
ISimpleCOMObject2). The source for this STA object is located in the
"SimpleCOMObject2" folder in the ZIP file accompanying this article. The
ISimpleCOMObject2 interface includes just one method: TestMethod1().
TestMethod1() is very simple. It displays a message box which shows the ID of the
thread in which the method is running on:
STDMETHODIMP CSimpleCOMObject2::TestMethod1()
{
TCHAR szMessage[256];
sprintf (szMessage, "Thread ID : 0x%X", GetCurrentThreadId());
::MessageBox(NULL, szMessage, "TestMethod1()", MB_OK);
return S_OK;
}
int main()
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2Ptr spISimpleCOMObject2;
spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2 -> TestMethod1();
hThread = CreateThread
(
(LPSECURITY_ATTRIBUTES)NULL, // SD
(SIZE_T)0, // initial stack size
(LPTHREAD_START_ROUTINE)ThreadFunc, // thread function
(LPVOID)NULL, // thread argument
(DWORD)0, // creation option
(LPDWORD)&dwThreadId // thread identifier
);
WaitForSingleObject(hThread, INFINITE);
spISimpleCOMObject2 -> TestMethod1();
}
::CoUninitialize();
return 0;
}
... and a utility function named DisplayCurrentThreadId() that shows a message box
displaying the ID of the currently running thread:
What we have demonstrated here is the straightforward creation of two STAs which were
initialized by main()'s thread and by ThreadFunc()'s thread. main()'s STA then
proceeds to contain the STA object spISimpleCOMObject2. ThreadFunc()'s thread will
also contain the STA objects spISimpleCOMObject2A and spISimpleCOMObject2B. The
following example illustrates the above:
An important point to note is that spISimpleCOMObject2, spISimpleCOMObject2A and
spISimpleCOMObject2B are all instances of the same coclass yet it is possible that they
reside in separate STAs. For a standard STA object, what matters is which STA first
instantiates it.
Notice also in this example that we had not supplied any message loops in both main()
and ThreadFunc(). They are not needed. The objects in both STAs are used within their
own Apartments and are not used across threads. We even included a call to
WaitForSingleObject() in main() and it did not cause any trouble. There were no
occasions to use the hidden windows of these STA objects. No messages were posted to
these hidden windows and so no message loops were needed.
In the next section, we will discuss something known as the Default STA. We will also
demonstrate it by example codes. The examples will also enhance the validity of the
above example which we have just studied.
What happens when an STA object gets instantiated inside a non-STA thread? Let us look
at a second set of example codes which will be presented below. This new set of source
codes are listed in "Test Programs\VCTests\DemonstrateDefaultSTA\VCTest01". It also
uses the example STA COM object of coclass SimpleCOMObject2 (implements interface
ISimpleCOMObject2) which was seen in the last example. The current example also uses
the utility function DisplayCurrentThreadId() that shows a message box displaying
the ID of the thread currently running.
int main()
{
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2Ptr spISimpleCOMObject2;
/* If a default STA is to be created and used, it will be created
*/
/* right after spISimpleCOMObject2 (an STA object) is created. */
spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2 -> TestMethod1();
}
::CoUninitialize();
return 0;
}
What happened was that spISimpleCOMObject2 will live inside a default STA. All STA
objects in a process which are created inside non-STA threads will reside in the default
STA.
This default STA was created at the same point when the affected object
(spISimpleCOMObject2, in our example) is created. This is illustrated by the following
diagram:
As can be seen in the above diagram, since spISimpleCOMObject2 lives in the default
STA and not within main()'s MTA, main()'s call to spISimpleCOMObject2 ->
TestMethod1() is an inter-apartment method call. This requires marshalling, and hence
what main() receives from COM is not an actual pointer to spISimpleCOMObject2 but a
proxy to it.
And since inter-apartment calls are actually performed, the default STA must contain a
message loop. This is provided for by COM.
Developers new to the world of COM Apartments please note well this intriguing
phenomenon: that even though a call to CreateInstance() or CoCreateInstance() is
made inside a thread, the resulting object can actually be instantiated in another thread.
This is performed transparently by COM behind the scenes. Please therefore take note of
this kind of subtle maneuvering by COM especially during debugging.
Let us now look at a more sophisticated example. This time, we use the sources listed in
"Test Programs\VCTests\DemonstrateDefaultSTA\VCTest02". This new set of sources
also use the same STA COM object of coclass SimpleCOMObject2 (implements interface
ISimpleCOMObject2) which was seen in the last example. The current example also uses
the utility function DisplayCurrentThreadId() that shows a message box displaying
the ID of the thread currently running when this function is invoked.
int main()
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2Ptr spISimpleCOMObject2;
spISimpleCOMObject2.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2 -> TestMethod1();
hThread = CreateThread
(
(LPSECURITY_ATTRIBUTES)NULL, // SD
(SIZE_T)0, // initial stack size
(LPTHREAD_START_ROUTINE)ThreadFunc, // thread function
(LPVOID)NULL, // thread argument
(DWORD)0, // creation option
(LPDWORD)&dwThreadId // thread identifier
);
WaitForSingleObject(hThread, INFINITE);
spISimpleCOMObject2 -> TestMethod1();
}
::CoUninitialize();
return 0;
DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
DisplayCurrentThreadId();
if (1)
{
ISimpleCOMObject2Ptr spISimpleCOMObject2A;
ISimpleCOMObject2Ptr spISimpleCOMObject2B;
spISimpleCOMObject2A.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2B.CreateInstance(__uuidof(SimpleCOMObject2));
spISimpleCOMObject2A -> TestMethod1();
spISimpleCOMObject2B -> TestMethod1();
}
::CoUninitialize();
return 0;
}
What we have shown here is a more complicated example of the creation and use of the
default STA. spISimpleCOMObject2 is an STA object that got instantiated inside a non-
STA thread (main()'s thread). spISimpleCOMObject2A and spISimpleCOMObject2B
were also instantiated inside a non-STA thread (ThreadFunc()'s thread). Therefore, all
three objects spISimpleCOMObject2, spISimpleCOMObject2A and
spISimpleCOMObject2B will all reside in the default STA which is first created when
spISimpleCOMObject2 is created.
I strongly encourage the reader to modify the source codes and see different results.
Change one or more ::CoInitializeEx() calls from using
COINIT_APARTMENTTHREADED to COINIT_MULTITHREADED and vice versa. Put a
breakpoint in "CSimpleCOMObject2::TestMethod1()" to see the difference when it is
invoked from an STA thread and when it is invoked from an MTA thread.
In the latter case, you will see that the invocation is indirect and that some RPC calls are
involved (see diagram below).
These calls are part of the marshalling code put in motion during inter-apartment calls.
There is another type of default STA known as the Legacy STA. This STA is where the
legacy COM objects will reside in. By legacy, we mean those COM components that
have no knowledge of threads whatsoever. These objects must have their
ThreadingModel registry entry set to "Single" or have simply left out any
ThreadingModel entry in the registry.
The important point to note about these Legacy STA objects is that all instances of these
objects will be created in the same STA. Even if they are created in a thread initialized
with ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED), they will still live and
run in the legacy STA if it has already been created.
The legacy STA is usually the very first STA created in a process. If a legacy STA object
is created before any STA is created, one will be created by the COM sub-system.
The advantage of developing a legacy STA object is that all access to all instances of such
objects are serialized. You do not need any inter-apartment marshalling between any two
legacy STA objects. However, non-legacy STA objects living in non-legacy STAs that
want to make calls to legacy-STA objects must, nevertheless, arrange for inter-apartment
marshalling. The converse (legacy-STA objects making calls to non-legacy STA objects
living in non-legacy STAs) also requires inter-apartment marshalling. Not a very
attractive advantage, I think.
Let us showcase two examples. The first example we will cover uses an example Legacy
STA COM object of coclass LegacyCOMObject1. The source codes for this COM object
is listed in "LegacyCOMObject1". This COM object functions similarly with the COM
object of coclass SimpleCOMObject2 which we have seen in previous examples.
LegacyCOMObject1 also has a method named TestMethod1() which also displays the ID
of the thread in which the TestMethod1() function is executing.
The test program which uses LegacyCOMObject1 has its source codes listed in "Test
Programs\VCTests\DemonstrateLegacySTA\VCTest01". This current test program also
uses the same utility function DisplayCurrentThreadId() that shows a message box
displaying the ID of the thread currently running when this function is invoked.
A Legacy STA object behaves very much like a standard STA object as the above two
examples show. However, there is a difference: all Legacy STA objects can only be
created inside the same STA thread. We will demonstrate this with yet another example
code.
The next example code also uses the same LegacyCOMObject1 object which was
demonstrated in the last example. This current test program also uses the same utility
function DisplayCurrentThreadId() that shows a message box displaying the ID of the
thread currently running when this function is invoked. The example code is listed in
"Test Programs\VCTests\DemonstrateLegacySTA\VCTest02".
int main()
{
HANDLE hThread = NULL;
DWORD dwThreadId = 0;
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
DisplayCurrentThreadId();
if (1)
{
ILegacyCOMObject1Ptr spILegacyCOMObject1;
spILegacyCOMObject1.CreateInstance(__uuidof(LegacyCOMObject1));
spILegacyCOMObject1 -> TestMethod1();
hThread = CreateThread
(
(LPSECURITY_ATTRIBUTES)NULL,
(SIZE_T)0,
(LPTHREAD_START_ROUTINE)ThreadFunc,
(LPVOID)NULL,
(DWORD)0,
(LPDWORD)&dwThreadId
);
ThreadMsgWaitForSingleObject(hThread, INFINITE);
spILegacyCOMObject1 -> TestMethod1();
}
::CoUninitialize();
return 0;
}
DWORD WINAPI ThreadFunc(LPVOID lpvParameter)
{
::CoInitializeEx(NULL, COINIT_MULTITHREADED);
DisplayCurrentThreadId();
if (1)
{
ILegacyCOMObject1Ptr spILegacyCOMObject1A;
ILegacyCOMObject1Ptr spILegacyCOMObject1B;
spILegacyCOMObject1A.CreateInstance(__uuidof(LegacyCOMObject1));
spILegacyCOMObject1B.CreateInstance(__uuidof(LegacyCOMObject1));
spILegacyCOMObject1A -> TestMethod1();
spILegacyCOMObject1B -> TestMethod1();
}
::CoUninitialize();
return 0;
}
Let us analyze this latest test program. The thread executing main() enters a standard
STA. This STA is the first STA created in the process. Recall that the first STA created in
a process is also the Legacy STA, hence the main()'s STA is the Legacy STA. Now,
spILegacyCOMObject1 (in main()) is created as a normal STA object and it resides in
the same STA as the one just created in main().
When the second thread (headed by ThreadFunc()) starts up, it is started as an MTA.
Hence any STA object created inside this thread cannot live in this MTA (it cannot use
ThreadFunc()'s thread). Both spILegacyCOMObject1A and spILegacyCOMObject1B are
STA objects and hence they cannot live inside ThreadFunc()'s MTA. Now, if
spILegacyCOMObject1A and spILegacyCOMObject1B are normal STAs, a new STA will
be created for them to live in. However, they are Legacy STAs and so they must live in
the legacy STA (if one already exists, and one already does exist).
The end result is that they will be accommodated in the Legacy STA created in main()'s
thread. This is why, when you invoke TestMethod1() from ThreadFunc(), the call is
actually marshaled to main()'s thread. There is actually inter-apartment marshalling
between ThreadFunc()'s MTA apartment (where the TestMethod1() call originates) and
main()'s STA apartment (where the TestMethod1() call is executed).
Thus far, we have discussed COM servers implemented inside DLLs. However, this
article will not be complete without touching on COM servers implemented in EXEs. My
aim is to show how Apartments, the STA in particular, are implemented inside an EXE
server. Let us start with examining two of the main differences between a DLL server and
an EXE server.
When COM wishes to create a COM object which is implemented inside a DLL, it loads
the DLL, connects with its exported DllGetClassObject() function, calls it, and obtains
a pointer to the IClassFactory interface of the class factory object of the COM object. It
is from this IClassFactory interface pointer that the COM object is created.
The story with EXE Servers has the same eventuality: obtaining the IClassFactory
interface pointer of the class factory object of the COM object to be created and then
creating the COM object through it. What happens before that is the difference between a
DLL server and an EXE Server.
A DLL server exports the DllGetClassObject() function for COM to extract the class
factory but an EXE server cannot export any function. An EXE server instead has to
register its class factory in the COM sub-system when it starts up, and then revoke the
class factory when it shuts down. This registration is done via the API
CoRegisterClassObject().
Objects implemented in an EXE server do not set this registry value. Instead, the
Apartment Model of the thread which registers the object's class factory determines the
object's Apartment Model:
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
...
...
...
IUnknown* pIUnknown = NULL;
DWORD dwCookie = 0;
pCExeObj02_Factory -> QueryInterface(IID_IUnknown,
(void**)&pIUnknown);
if (pIUnknown)
{
hr = ::CoRegisterClassObject
(
CLSID_ExeObj02,
pIUnknown,
CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED,
&dwCookie
);
pIUnknown -> Release();
pIUnknown = NULL;
}
In the above code snippet, we are attempting to register a class factory for the
CLSID_ExeObj02 COM object inside a thread. Note the call to
::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) at the beginning. This call
indicates to COM that CLSID_ExeObj02 COM objects will live in an STA. The thread
which called CoRegisterClassObject() is the lone thread inside this STA. This implies
that there will be a message loop inside this thread and that all access to any
CLSID_ExeObj02 object created by any client are serialized by this message loop.
Aside from the above two differences, take note that while it is possible that STA objects
inside a DLL server receive method calls only from inside its owning STA thread, all
method calls from a client to an STA object inside an EXE COM server will invariably be
invoked from an external thread. This implies the use of marshalling proxies and stubs
and, of course, a message loop inside the object's owning STA thread.
As usual, we shall attempt to demonstrate STAs inside COM EXE Servers via an
example code. The example code for this section is rather elaborate. It can be found in the
following folder: "Test Programs\VCTests\DemonstrateExeServerSTA" in the sample
code that accompanies this article. There are three parts to this set of sample code:
Please note that in order to use the ExeServerImpl COM server, you will need to
compile code in "Implementation\ExeServerImpl" and then register the resultant
ExeServerImpl.exe by typing the following command in a command prompt window:
ExeServerImpl RegServer
The Interface
The Implementation
The code in the Implementation part provides an implementation of the interfaces and
coclass's described in the Interface part. Except for CExeObj02, each of the
implementation of TestMethod1() contains only a message box display:
STDMETHODIMP CExeObj01::TestMethod1()
{
TCHAR szMessage[256];
sprintf (szMessage, "0x%X", GetCurrentThreadId());
::MessageBox(NULL, szMessage, "CExeObj01::TestMethod1()", MB_OK);
return S_OK;
}
STDMETHODIMP CExeObj03::TestMethod1()
{
TCHAR szMessage[256];
sprintf (szMessage, "0x%X", GetCurrentThreadId());
::MessageBox(NULL, szMessage, "CExeObj03::TestMethod1()", MB_OK);
return S_OK;
}
The purpose of doing this is to show the ID of the thread which is executing when each of
the methods is invoked. This should match with the ID of their containing STA thread. I
have made CExeObj02 a little special. This C++ class provides an implementation of
IExeObj02. It also contains a pointer to an IExeObj01 object:
CExeObj02::CExeObj02()
{
::CoCreateInstance
(
CLSID_ExeObj01,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj01,
(LPVOID*)&m_pIExeObj01
);
}
The purpose of doing this is to show later that CExeObj02 and the object behind
m_pIExeObj01 will run in separate STAs. Take a look at this class' TestMethod1()
implementation:
STDMETHODIMP CExeObj02::TestMethod1()
{
TCHAR szMessage[256];
sprintf (szMessage, "0x%X", GetCurrentThreadId());
::MessageBox(NULL, szMessage, "CExeObj02::TestMethod1()", MB_OK);
return m_pIExeObj01 -> TestMethod1();
}
Two message boxes will be displayed: the first one showing CExeObj02's thread ID and
the second will show m_pIExeObj01's thread ID. These IDs will be different as will be
seen later on when we run the client code.
I have left out some code in WinMain() that pertains to EXE server registration and
unregistration which are not relevant to our discussion here. I have narrowed down the
code to show only the runtime class factory registration process.
These are simple helper functions and, to avoid digression, I will not discuss them in this
article but to provide only a summary of what these functions do:
Whenever the EXE COM Server starts up, it registers all three class factories (albeit not
all of them are performed in WinMain()'s thread).
ThreadFunc_
3 RegisterExeObj03Factory()
CExeObj03_Factory CLSID_ExeObj03 STA
The Client
Let us move on now to the Client. The Client code is simple. It consists of a main()
function that instantiates two instances each of coclass'es CLSID_ExeObj01,
CLSID_ExeObj02 and CLSID_ExeObj03. Each instantiation is referenced by a pointer to
interfaces IExeObj01, IExeObj02 and IExeObj03 respectively:
int main()
{
IExeObj01* pIExeObj01A = NULL;
IExeObj01* pIExeObj01B = NULL;
IExeObj02* pIExeObj02A = NULL;
IExeObj02* pIExeObj02B = NULL;
IExeObj03* pIExeObj03A = NULL;
IExeObj03* pIExeObj03B = NULL;
HRESULT hr = ::CoInitializeEx(NULL, COINIT_MULTITHREADED);
::CoCreateInstance
(
CLSID_ExeObj01,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj01,
(LPVOID*)&pIExeObj01A
);
::CoCreateInstance
(
CLSID_ExeObj01,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj01,
(LPVOID*)&pIExeObj01B
);
::CoCreateInstance
(
CLSID_ExeObj02,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj02,
(LPVOID*)&pIExeObj02A
);
::CoCreateInstance
(
CLSID_ExeObj02,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj02,
(LPVOID*)&pIExeObj02B
);
::CoCreateInstance
(
CLSID_ExeObj03,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj03,
(LPVOID*)&pIExeObj03A
);
::CoCreateInstance
(
CLSID_ExeObj03,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj03,
(LPVOID*)&pIExeObj03B
);
...
...
...
}
The Client code then proceeds to call each interface pointer's TestMethod1() method
before releasing all interface pointers:
if (pIExeObj01A)
{
pIExeObj01A -> TestMethod1();
}
if (pIExeObj01B)
{
pIExeObj01B -> TestMethod1();
}
if (pIExeObj02A)
{
pIExeObj02A -> TestMethod1();
}
if (pIExeObj02B)
{
pIExeObj02B -> TestMethod1();
}
if (pIExeObj03A)
{
pIExeObj03A -> TestMethod1();
}
if (pIExeObj03B)
{
pIExeObj03B -> TestMethod1();
}
Let us observe what will happen when the Client application runs:
1. When the first call to ::CoCreateInstance() is made, COM will launch our
EXE COM server.
2. Our COM server will then run its WinMain() function. The first visible thing it
will do is to display WinMain()'s thread ID in a message box. Let's say this is
thread_id_1.
3. Next, the class factory for the CLSID_ExeObj01 COM object is registered
within WinMain()'s thread. Hence CLSID_ExeObj01 COM objects will live in
the STA headed by WinMain()'s thread.
4. Our COM server will then launch the thread headed by
ThreadFunc_RegisterExeObj02Factory() which will register the class
factory for the CLSID_ExeObj02 COM object. The ID for this thread is
displayed by a message box at the start of the thread. Let's say this is
thread_id_2.
5. CLSID_ExeObj02 COM objects will live in the STA headed by the thread with
ID thread_id_2.
6. Our COM server will then launch the thread headed by
ThreadFunc_RegisterExeObj03Factory() which will register the class
factory for the CLSID_ExeObj03 COM object. The ID for this thread is
displayed by a message box at the start of the thread. Let's say this is
thread_id_3.
7. CLSID_ExeObj03 COM objects will live in the STA headed by the thread with
ID thread_id_3.
8. Back to the client code. When TestMethod1() is invoked on pIExeObj01A, the
ID of the thread which is executing is displayed. You will note that this is
thread_id_1 which is consistent with point 3 above.
9. The same ID will be displayed when TestMethod1() is invoked on
pIExeObj01B.
10. When TestMethod1() is invoked on pIExeObj02A, two thread IDs will be
displayed one after the other. The first one is the ID of the thread executing
when pIExeObj02A -> TestMethod1() is invoked, and this is thread_id_2
which is consistent with point 5 above.
11. When the second message box is displayed, we will see the ID of the thread
running when the CLSID_ExeObj01 COM object contained inside pIExeObj02A
is invoked. This is not thread_id_2 but thread_id_1! This is perfectly in line
with point 3 above.
12. The same pair of IDs will be displayed when TestMethod1() is invoked on
pIExeObj02B.
13. When pIExeObj03A -> TestMethod1() and pIExeObj03B -> TestMethod1()
are invoked in the statements that follow, we will see that the ID of the
thread executing them is thread_id_3. This is again consistent with point 7
above.
We have thus demonstrated STAs as used inside a COM EXE Server. I strongly
encourage the reader to experiment with the code and see the effects of changing one or
more threads from STAs to MTAs. It's a fun way to learn.
Before I conclude this last major section, please allow me to present two short variations
to the Implementation code. The first shows the dramatic effects of not providing the
appropriate message loop inside a class registration thread. The second shows the
completely harmless effects of not providing one!
Variation 1
Let us examine the first case. In the EXE COM server code's main.cpp file, we modify
the ThreadFunc_RegisterExeObj02Factory() function as follows:
We simply add a Sleep() statement right above the message loop. Compile the EXE
COM server again. Run the client in debug mode (so that you can observe what happens
when coclass CLSID_ExeObj02 is instantiated as in the following call to
CoCreateInstance()):
::CoCreateInstance
(
CLSID_ExeObj02,
NULL,
CLSCTX_LOCAL_SERVER,
IID_IExeObj02,
(LPVOID*)&pIExeObj02A
);
You will note that this call will appear to hang. But hold on, if you had patiently waited
for about 20 seconds, the call will go through. What happened? Well, turns out that
because CLSID_ExeObj02 is an STA object, the call to CoCreateInstance() resulted in
a need to communicate with the message loop of the thread that registered the
CLSID_ExeObj02 class factory.
By blocking the thread with a Sleep() statement, the thread's message loop does not get
serviced. The call to create instance will not return in this case. But once the Sleep()
statement returns, the message loop is started and the create instance call is serviced and
will return in time.
Note therefore the importance of the message loop in a COM EXE Server STA thread.
Variation 2
This time, we change the thread into an MTA thread, set Sleep()'s parameter to
INFINITE, and comment out the message loop altogether.
You will find that the call to ::CoCreateInstance() on coclass CLSID_ExeObj02 in the
client will go through successfully albeit CLSID_ExeObj02 is now an MTA object and the
calls to its TestMethod1() method may display different thread IDs.
What we have shown clearly here is that as long as the MTA thread that registers a class
factory remains alive (via Sleep(INFINITE)), calls to the class factory goes through
(without the need for any message loop, by the way).
In Conclusion
I certainly hope that you have benefited from the explanatory text as well as the example
code of this long article. I have done my level best to be as thorough and exhaustive as
possible to lay a strong foundation on the concepts of Single-Threaded Apartments.
In this part one, I have demonstrated a few inter-apartment method calls for which COM
has already paved the way. We have also seen how COM automatically arranges for
objects to be created in the appropriate apartment threads. Proxies and stubs are generated
internally and the marshalling of proxies are performed transparently without the
developers' knowledge.
In part two, we will touch on more advanced features of COM that pertain to STAs. We
shall show how to perform explicit marshalling of COM object pointers from one
apartment to another. We will also show how an object can fire events from an external
thread. Lower-level codes will be explored.
Please note that I have changed the title of this article from "TEventHandler - A C++
COM Event Handler For IDispatch-Based Events" to the current title "Understanding
COM Event Handling". The latter, I believe, is a better title which gives a more accurate
picture of the intended theme of this article - i.e., to expound carefully the internal
mechanisms behind COM event handling.
Introduction
If you have ever done any development work involving the use of COM objects, chances
are you would have encountered the need for COM object event handling. Visual Basic
users will know how simple it is to connect with the event interface(s) of the COM (or
ActiveX) objects. The VB IDE lays out the event handling function codes nicely for the
user. All the user has to do is to fill in the details of the event handling functions.
For Visual C++ users, this is not always so straight forward. If your COM object happens
to be an ActiveX control and you are using MFC, then yes, the Visual C++ IDE provides
Wizards that can help you generate event handler function stubs. All the necessary codes
(e.g., inserting the event sink map and event entry macros) are done automatically for
you.
But what if your COM object is not an ActiveX control? What if you are using straight
COM objects which also fire events and you need to handle those events?
If you are an MFC user, you may want to tinkle with the various MFC macros to see if
you can fit in an event handler function into your code either manually or via the Wizard.
I personally believe this is possible. But you need to be armed with an intimate
knowledge of MFC and its generated macros.
If you do not use MFC, you may want to experiment with ATL codes (e.g.,
IDispEventImpl, BEGIN_SINK_MAP, SINK_ENTRY_EX, etc.) to perform event handling.
The ATL macro codes are certainly not simple but they are well-documented in MSDN
and they do provide standard handling mechanisms.
In this article, I will go back to basics and seek to explain the fundamental principles of
how event handling is done in COM. I will also provide a C+ class which serves as a
basic and simple (at least in terms of code overhead) facilitator for COM Object Event
Handling.
Incoming Interfaces
When we develop our usual COM objects, we provide implementations for interfaces
(defined in an IDL file) which we write ourselves or have been supplied to us. Such
implementations facilitate what are known as "incoming" interfaces. By "incoming", we
imply that the object "listens" to its client. That is, the client calls methods of the
interface and, in this way, "talks" to the object.
As Kraig Brockschmidt puts it so well in his book "Inside OLE", many COM objects
themselves have useful things to say to their clients. And clients may want to listen to
COM objects too. If such a two-way dialog is desired, something known as an
"outgoing" interface is required.
The term "outgoing" is used in the context of the COM object. It is outgoing in the
perspective of the COM object. Imagine a situation in which the role of "talker" and
"listener" is reversed as shown in the diagram below:
A COM object that supports one or more outgoing interfaces is known as a Connectable
Object or a Source. A connectable object can support as many outgoing interfaces as it
likes. Each method of the outgoing interface represents a single event or request.
All COM objects, regardless of whether they are non-visual COM objects or ActiveX
controls (generated manually or via MFC or ATL), use the same mechanism
(connectability) for firing events to their clients.
Events are used to tell a client that something of interest has occurred in the object - a
property has changed or the user has clicked a button. Events are particularly important
for COM controls. Events are fired by COM objects and no response from the client is
expected. In other words, they are simple notifications.
Requests, on the other hand, is how a COM object asks the client a question and expects
a response in return.
Events and requests are similar to Windows messages, some of which inform a window
of an event (e.g., WM_MOVE) and some will ask for information from the window (e.g.,
WM_QUERYENDSESSION).
Sinks
In both cases, the client of the COM object must listen to what the object has to say and
then use that information appropriately. It is the client, therefore, that implements the
outgoing interfaces which are also known as sinks (I really dislike this name but it has
become common and ubiquitous in the world of COM and .NET).
From a sink's perspective, this outgoing interface is actually incoming. The sink listens to
the COM object through it. In this context, the connectable COM object plays the role of
a client.
Let us take a helicopter view of the entire communications situation. There are three
participants:
In order for the COM object to communicate back to the client in the other direction, the
COM object must somehow obtain a pointer to an outgoing interface implemented
somewhere in the client. Through this pointer, the COM object will send events and
requests to the client.
This somewhere is the Sink. Let us illustrate the above with a simple diagram:
Note that the Sink is an object by itself. The Sink provides the implementation for one or
more outgoing interfaces. It is also usually strongly tied to the other parts of the client's
code because the whole idea of implementing a sink is for the client code to be able to
react to an event and/or to respond to some request from the COM object.
But how does a COM object connect to a Sink in the first place? This is where the notion
of Connection Points and Connection Point Containers come in. We will explore this in
detail in the next section.
Connection Points And Connection Point Containers
For each outgoing interface that a COM object supports (note the use of the word
support, the COM object itself does not implement this interface, it invokes it), the COM
object exposes a small object called a connection point. This connection point object
implements the IConnectionPoint interface.
It is through this IConnectionPoint interface that the client passes its Sink's outgoing
interface implementation to the COM object. Reference counts of these
IConnectionPoint objects are kept by both the client and the COM object itself to
ensure the lifespan of the two-way communications.
The method to call (from the client side) to establish event communication with the COM
object is the IConnectionPoint::Advise() method. The converse of Advise() is
IConnectionPoint::Unadvise() which terminates a connection. Please refer to MSDN
documentation for more details of these methods.
Hence, via IConnectionPoint interface method calls, a client can start listening to a set
of events from the COM object. Note also that because a COM object maintains a
separate IConnectionPoint interface for every outgoing interface it supports, a client
must be able to use the correct connection point object for every sink it implements.
How then, does the Client choose the appropriate connection point for a sink? In comes
Connection Point Containers. An object which is connectable must also be a Connection
Point Container. That is, it must implement the IConnectionPointContainer interface.
Through this interface, a Client requests for the appropriate Connection Point object of an
outgoing interface.
When a client wants to connect a Sink to a Connection Point, it asks the Connection Point
Container for the Connection Point object for the outgoing interface implemented by that
Sink. When it receives the appropriate connection point object, the Client passes the
Sink's interface pointer to that connection point.
if (pIConnectionPointContainerTemp)
{
pIConnectionPointContainerTemp ->
FindConnectionPoint(__uuidof(ISomeEventInterface),
&m_pIConnectionPoint);
pIConnectionPointContainerTemp -> Release();
pIConnectionPointContainerTemp = NULL;
}
if (m_pIConnectionPoint)
{
m_pIConnectionPoint -> Advise(pIUnknown, &m_dwEventCookie);
}
1. We first QueryInterface() the Sink object itself for its IUnknown interface
pointer. This IUnknown pointer will be used later in the call to
IConnectionPoint::Advise().
2. Having successfully obtained the IUnknown pointer, we next
QueryInterface() the object behind pISomeInterface for its
IConnectionPointContainer interface.
3. Having successfully obtained the IConnectionPointContainer interface
pointer, we use it to find the appropriate Connection Point object for the
outgoing interface ISomeEventInterface.
4. If we are able to obtain this Connection Point object (it will be represented by
m_pIConnectionPoint), we will proceed to call its Advise() method.
5. From here onwards, whenever the COM object behind pISomeInterface fires
an event to the sink (by calling one of the methods of
ISomeEventInterface), the corresponding method implementation in the
Sink object will be invoked.
I certainly hope that the above introductory sections on Event Handling in COM,
Connection Points and Connection Point Containers will have served to provide the
reader with a clear understanding of the basics of event handling. It is worth re-
mentioning that the above principles are used whatever the type of COM object is
involved (e.g., straight COM object, ActiveX controls, etc.).
With the basics explained thoroughly, we shall proceed to expound on the sample source
codes, especially the TEventHandler template class.
I will attempt to explain TEventHandler by running through some example codes. Please
refer to the source files which are contained in the TEventHandler_src.zip file. In the set
of sample codes, I have provided two sets of projects:
• EventFiringObject
• TestClient
EventFiringObject
The EventFiringObject project contains the code for a simple COM object which
implements an interface named IEventFiringObject. This COM object is also a
Connection Point Container which recognizes the _IEventFiringObjectEvents
connection point.
The relevant IDL constructs for this COM object is listed below for discussion purposes:
[
object,
uuid(8E396CC0-A266-481E-B6B4-0CB564DAA3BC),
dual,
helpstring("IEventFiringObject Interface"),
pointer_default(unique)
]
interface IEventFiringObject : IDispatch
{
[id(1), helpstring("method TestFunction")]
HRESULT TestFunction([in] long lValue);
};
[
uuid(32F2B52C-1C07-43BC-879B-04C70A7FA148),
helpstring("_IEventFiringObjectEvents Interface")
]
dispinterface _IEventFiringObjectEvents
{
properties:
methods:
[id(1), helpstring("method Event1")] HRESULT Event1([in] long lValue);
};
[
uuid(A17BC235-A924-4FFE-8D96-22068CEA9959),
helpstring("EventFiringObject Class")
]
coclass EventFiringObject
{
[default] interface IEventFiringObject;
[default, source] dispinterface _IEventFiringObjectEvents;
};
In the EventFiringObject project, we implement a C++ ATL class named
CEventFiringObject which implements the specifications of coclass
EventFiringObject. CEventFiringObject provides a simple implementation of the
TestFunction() method. It simply fires Event1 which is specified in
_IEventFiringObjectEvents.
TestClient
The client code centers around the CTestClientDlg class which is derived from
CDialog. In TestClientDlg.h, notice that we declare an instance of a smart pointer object
which will be tied to the COM object which implements IEventFiringObject:
We also create a button in our simple dialog box labeled "Call Test Function". In the click
handler for this button, we invoke the TestFunction() of our
m_spIEventFiringObject:
Thus far, we have dealt with mostly typical COM client code. The fun begins when we
invoke TestFunction(). We know that TestFunction() will cause the
m_spIEventFiringObject COM object to fire the Event1 event. This is where the real
action starts.
The TEventHandler Class
The predefined method of the client must be defined using the following signature:
In the context of our example code, our usage of TEventHandler can be represented by
the following diagram:
What TEventHandler Is Not Designed To Do
The answer is that the TEventHandler class is not primarily designed to simplify the
handling of the parameters of the event methods (although this might be possible, see my
comments on this later in this section).
It is designed to be a sink object. It is meant to readily make available (for a client) a sink
which can be hooked up to receive the dispinterface-based event of a COM object. And
then have the sink automatically call the mirror Invoke() method of the client.
Note that using C++ templates alone, it will not be possible to anticipate in advance the
return values and parameter lists of event methods. This would require the work of
Wizards which can read all these information from the type libraries associated with the
connectable COM objects and then generating function codes which match the signatures
of event methods.
Interfaces that derive from IDispatch are also known as dispinterfaces (for dispatch
interfaces). Interfaces, in general (not just event interfaces), that are dispinterfaces are
very common. They are common because of the long-time need for COM to support
Visual Basic.
The IDispatch interface is the basis behind Visual Basic's achievement of something
known as late binding which means the act of programmatically constructing a function
call (together with the inclusion of parameters and the receipt of return values) at
runtime. It is perhaps the most generic and flexible of all COM interfaces.
Take note that I'm using the term late binding in a generic way (I'm not referring to the
C++ concept of vtables which is another implementation of late binding).
IDispatch can be used to define a virtual interface the methods of which are called via
the IDispatch::Invoke() method. This system of runtime function invocation is known
as automation (formerly OLE-automation).
Here, we are asking a connection point container to return to us a connection point that
supports the outgoing interface that is identified by the DIID which is equivalent to the
result of __uuidof(ISomeEventInterface).
Visual Basic applications can only handle ActiveX object events through sinks which are
dispinterface-based. This is natural both because of the fact that Visual Basic cannot
handle non-dispinterface-based events, and also because of the need to handle events
fired from any and all ActiveX objects.
The central point behind this is that while event interfaces need not be dispinterface-
based (their methods can be of any signature, the only mandate is that the event interface
must also derive from IUnknown), Visual Basic is not able to internally anticipate the
design of these custom event interfaces and to generate Sinks for them.
Furthermore, the types of method return values and parameters must be confined to those
that Visual Basic is able to understand and internally process.
Hence, the only way to standardize the handling of event interfaces is to require that they
be derived from IDispatch, and that the return and parameter types be from a wide but
limited ranged set. This set of types is known as the automation-compatible-types.
Like Visual Basic, it is not possible for us to design the TEventHandler template class to
be a Sink for custom event interfaces. Remember that we must actually implement the
methods of the event interface that TEventHandler is to be a Sink for. Hence, I decided
that TEventHandler should handle only dispinterface-based events. And there are plenty
of COM object sources which support them.
Code Details
Note that TEventHandler is derived from IDispatch. However, note, it need not
implement all of the methods of IDispatch. Only the basic AddRef(), Release(),
QueryInterface() and the Invoke() methods are required at minimum.
1. event_handler_class
2. device_interface
3. device_event_interface
In out example use case, this parameter will be the CTestClientDlg class. The
CTestClientDlg class will contain the invocation function:
HRESULT CTestClientDlg::OnEventFiringObjectInvoke
(
IEventFiringObjectEventHandler* pEventHandler,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
)
It is in this function that CTestClientDlg will handle events fired from the
EventFiringObject COM object.
The "device_interface" parameter refers to the interface type of the COM object
(whose event we are trying to receive). In our example use case, this will be
IEventFiringObject.
All the above template parameters are used in order that the VC++ compiler be able to
generate a C++ class which has been tailored to contain methods, properties and
parameter types which match those of CTestClientDlg, IEventFiringObject and
_IEventFiringObjectEvents.
Hence, a customized class will eventually be created for further use in the code.
Usage
Usage of the TEventHandler class is simple and straightforward. Let us walk through
some example codes from TestClient:
Note that we are not instantiating an object here! We are merely defining a C++
class via the use of TEventHandler. We will call this new C++ class
IEventFiringObjectEventHandler. IEventFiringObjectEventHandler is the
customized class that we mentioned earlier.
HRESULT CTestClientDlg::OnEventFiringObjectInvoke
(
IEventFiringObjectEventHandler* pEventHandler,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
);
Please note that the reader must be familiar with the IDispatch::Invoke()
method in order to be able to interpret the values contained in the various
parameters of this method. See the example code in
OnEventFiringObjectInvoke() itself and refer to MSDN documentation for
further details.
31. To invoke the event handling code, I have included a button in the
CTestClientDlg dialog box, and the handler to this button will call the
EventFiringObject's TestFunction() method which will internally fire
Event1.
HRESULT CTestClientDlg::OnEventFiringObjectInvoke
(
IEventFiringObjectEventHandler* pEventHandler,
DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pdispparams,
VARIANT* pvarResult,
EXCEPINFO* pexcepinfo,
UINT* puArgErr
)
{
if (dispidMember == 0x01) // Event1 event.
{
// 1st param : [in] long lValue.
VARIANT varlValue;
long lValue = 0;
VariantInit(&varlValue);
VariantClear(&varlValue);
varlValue = (pdispparams -> rgvarg)[0];
lValue = V_I4(&varlValue);
TCHAR szMessage[256];
sprintf (szMessage, "Event 1 is fired with value : %d.",
lValue);
::MessageBox (NULL, szMessage, "Event", MB_OK);
}
return S_OK;
}
In Conclusion
I certainly do hope you will find the TEventHandler class useful. C++ templates are
really superb in generating generic code. There is already talk of C# providing template
features. I really can't wait to get my hands on this.
If you have any comments on TEventHandler, on how to improve it further, please drop
me an email anytime.
•
Introduction
This article is about COM and COM components. It explains some of the details behind
COM technology, through implementation of a simple COM component. The article
begins with some background information about COM and the reader is guided to
implement and improve a simple COM component in an example. The example starts
with implementation of a COM component and a COM client in a common file. The
improvement of the example is about the separation of the COM component form its
client. The client and the component are first separated to two different files, then the
component will be put on a DLL, which can be loaded into the address space of the client
and the final improvement is to register the component in Windows registry, such that the
client is no longer bound to the component, and is able to create it through the class
factory. In the following illustration, the relation between the component and its client is
shown with a chain, which will be broken totally when the component is created through
the class factory. In the last part, COM containment is explained by reusing the
implemented component in a new COM component. This article is only about part one
and the other parts are explained in two other articles. The demo application's code
(Client + 3 component Servers) is very similar to the example explained in the article and
only a window is used to visualize the component itself. The following image illustrates
part one and part two:
Assumption
Part one-Background
An object or component in COM is any structure that exposes its functionality through
the interface mechanism. In C++ applications, interfaces are defined as abstract base
classes. An interface is a C++ class that contains nothing but pure virtual member
functions. This means that the interface carries no implementation and only prescribes the
function signatures for some other class to implement. The pure abstract base classes
define the specific memory structure that COM requires for an interface, and when we
define a pure abstract base class, we are actually defining the layout for a block of
memory. Memory is not allocated for the structure until the abstract base class is
implemented in a derived class. So in the following piece of code, IComponent is both an
interface (because its memory layout follows the COM specification) and a pure abstract
base class.
//----------------------------------------------------------------//
// Definition of component's functionallity through an interface //
//----------------------------------------------------------------//
// The keyword "interface" is just an alias for "struct"
interface IComponent
{
// Methods for the implementation of the component's
functionalities:
virtual void __stdcall Function1()=0;
virtual void __stdcall Function2()=0;
virtual void __stdcall Function3()=0;
};
As shown in the following image, there are two parts to the block of memory defined by
a pure abstract base class which makes the interface of a COM component in C++ (the
top figure illustrates Component2 from the demo application):
1. The virtual function table or vtbl, which is an array of pointers that point to
the implementation of the virtual functions.
2. A pointer to the vtbl known as the vtbl-Pointer.
Conclusion: In COM, functionality of components is obtained through their interfaces,
which are addresses or entries to components' methods.
These three methods make up the IUnknown interface from which all other interfaces
inherit. All COM interfaces must inherit from IUnknown. This means that the first three
entries in the vtbl are the same for all COM interfaces. They are the addresses for the
implementation of the three methods in IUnknown interface. So in the following code, the
IComponent interface becomes a COM interface by inheriting from IUnknown interface:
//------------------------------------------------------------//
// Being a COM interface by inheriting from IUnknown interface//
//------------------------------------------------------------//
interface IComponent :public IUnknown;
{
virtual void __stdcall Function1()=0;
virtual void __stdcall Function2()=0;
virtual void __stdcall Function3()=0;
};
Because every COM component inherits from IUnknown, a client which has an IUnknown
interface pointer does not know what kind of interface pointer it has and it can just query
for other interfaces using the QueryInterface method. That's why this interface is called
Unknown interface or IUnknown. The following image shows the IComponent interface
being as a COM interface after inheriting from IUnknown interface.
When the component's functionality is defined through its methods (like Function1-
Function3) by a COM interface, the component's class can be defined by deriving a
class from this newly created COM interface. In the following example, this class is
called CComponent:
//----------------------------------//
//Definition of the Component's class
//----------------------------------//
class CComponent:public IComponent
{
public:
//---------------------------------------------------------------------
----//
//IUnknown methods,which are the basic oparation
//required for all COM components:
//---------------------------------------------------------------------
----//
virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();
//-------------------------------------------------------//
//The new methods, which the interface supports:
//-------------------------------------------------------//
virtual void __stdcall Function1();
virtual void __stdcall Function2();
virtual void __stdcall Function3();
private:
// Component's member data
long m_cRef;// The reference count variable
};
The following image illustrates the memory layout for the CComponent class:
As the image shows, an Interface pointer like the "this" pointer of the CComponent
class points to the vtbl pointer. In COM, a component is accessed just through methods
and never directly through variables, and pure abstract base classes have only pure virtual
functions and they do not have instance data. In order to call one of the component's
methods (like Function1), the IUnknown pointer can be used to query or ask for a pointer
that points to the IComponent interface, and by using that pointer, it will be possible to
call the desired method:
//---------------------------------------------//
//Getting a pointer to the IComponent interface:
//---------------------------------------------//
IComponent* pIComponent=NULL;
//The "IID_IComponent" is the interface ID for the IComponent interface
pIUnknown->QueryInterface(IID_IComponent,(void**) &pIComponent;
// Calling the component's method:
pIComponent->Function1();
As explained before, we can navigate between interfaces on a COM object through the
QueryInterface method. Interfaces encapsulate the implementation details of
components. Whenever we want to access a component, we want to obtain some desired
functionality and so it is obvious that we are aware of a component's functionality, before
we use it. If we do not know what functionality a component has, we cannot use it. It
means that a component's client should know what kind of functionalities or interfaces
the component supports. Every COM interface has an interface identifier, which can be
used by clients in order to query a particular interface. The interface identifiers are 128-
bit values, and in the previous piece of code, the IComponent interface is identified by
IID_IComponent, which is the interface identifier of the IComponent interface. So,
whenever a client wants to use a functionality of a component, it should know which
interface implements that functionality, and it's required that it delivers an interface
identifier when it queries that interface using the QueryInterface method. A component
is comparable with a window and its interfaces with the window's menu. A window's
menu operates like an interface; it is an entry to a variety of functionalities which can be
obtained by selecting its items with the mouse pointer. As a matter of fact, the interfaces
for the three components in the demo application are like menus, and the components
methods are accessible through the menu items. The following image shows one of these
components.
A window object has been used as a member data of the component in order to visualize
the component, and as the image shows, the component is a window with a menu which
operates as its interface, and the component's methods are accessible through menu items
using the mouse pointer like an interface pointer.
As explained before, the only way of getting access to a component's functionality is just
through its interfaces, so, once we get a pointer to a component's IUnknown interface, we
can actually access all the other interfaces (using the QueryInterface method)
supported by the component, and because all COM interfaces inherit from IUnknow
interface, every interface pointer is also an IUnknown interface pointer. A COM object is
created by using the new operator, and an interface pointer of the created object can be
obtained by converting the pointer which points to the object to an interface pointer or an
IUnknown pointer as shown below:
Interface Identifier
//-------------------------------------------//
// GUID definition:
//------------------------------------------//
typedef struct _GUID
{
unsigned long Data1; // 32 bits
unsigned short Data2; // 16 bits
unsigned short Data3; // 16 bits
unsigned char Data4[8];// 8*8 = 64 bits
//-----------------------------------------//
// Total bits = 128 bits
} GUID;
By now, you are able to make a simple COM object and let a client to use its
functionality. Using the following steps, you may make the example, which is shown in
the illustration at the beginning of the article.
You may download the code in each part and copy-paste some parts in order to make
your application quickly.
Step 1:
Using the AppWizard, create a simple Win32 Console application and choose an empty
project. This application will act as the COM client and will also hold the component
itself.
Step 2:
Create a source file with the extension of "cpp" and give it a name. I used the name
ClientAndComponent.cpp.
Step 3:
Let's decide a primitive functionality for the component, for example, the component
should be able to print the phrase "COM from scratch" on the screen. As mentioned
before, a COM component exposes its functionality through the interface mechanism, so
the need is to make an interface, and this interface should be derived from the IUnknown
interface if the component is going to be a COM component. So, let's call the interface
IComponent, derive it from the IUnknown interface and define the desired functionality in
a method called Print:
//-------------------------//
//Interface definition:
//-------------------------//
interface IComponent:IUnknown
{
//The new method for assigning component's functionality
virtual void __stdcall Print(const char* msg)=0;
};
Step 4:
In order to identify the IComponent interface, make an interface identifier for it by using
the tools UUIDGEN.EXE or GUIDGEN.EXE from your development environment:
//-----------------------------------------------//
//Interface identifier, which is a 128-bit value.//
//-----------------------------------------------//
// {853B4626-393A-44df-B13E-64CABE535DBF} string format
static const IID IID_IComponent =
{ 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3
{ 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4
Step 5:
The component's class can be defined by deriving it from the defined interface
(IComponent):
//----------------------------------//
//Definition of the Component's class
//----------------------------------//
class CComponent:public IComponent
{
public:
//---------------------------------------------------------------------
------//
//IUnknown methods,which are the basic oparation
//required for all COM components:
//---------------------------------------------------------------------
------//
virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv);
virtual ULONG __stdcall AddRef(){return 0;}
virtual ULONG __stdcall Release(){return 0;}
//-------------------------------------------------------//
//The new method, which the interface should support.
//-------------------------------------------------------//
virtual void __stdcall Print(const char* msg);
};
Step 6:
Step 8:
The final step is just to make the client, which uses the component's functionality through
the component's interface:
//----------------//
// Client
//----------------//
void main()
{
IUnknown* pIUnknown=CreateInstance();
IComponent* pIComponent=NULL;
pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent);
pIComponent->Print("COM from scratch.");
}
In order to control the life time of the object instantiated from the component's
class, it's necessary to have a variable which could be used as a reference count.
This variable should be incremented when the component is in use and
decremented when the client no longer uses the component. The AddRef() and
Release() methods can be used to increment and decrement this variable. So by
adding a private member variable to the component's class and calling the
AddRef() and Release() methods, it's possible to control the life time of the
component and free the memory which has been allocated for the component,
whenever this variable reaches the zero value. So in order to control the life time
of the component, redefine the component's class:
//
// Component
//
class CComponent : public IComponent
{
//IUnknown interface's methods:
virtual HRESULT __stdcall QueryInterface(const IID& iid,
void** ppv) ;
// method for incrementing the reference count variable
"m_cRef"
virtual ULONG __stdcall AddRef();
// method for decrementing the reference count variable
"m_cRef"
virtual ULONG __stdcall Release();
public:
CComponent() ;// Constructor
~CComponent();// Destructor
private:
};
/////////////////////////////////
CComponent::CComponent()
{
Print("Constructing the component...") ;
m_cRef=0;
}
////////////////////////////////////////
CComponent::~CComponent()
{
Print("Destructing the component...") ;
}
//////////////////////////////////////
ULONG __stdcall CComponent::AddRef()
{
Print("Incrementing the reference count variable...");
return InterlockedIncrement(&m_cRef);
}
• QueryInterface method:
One of the limits of the QueryInterface method in the program is that it doesn't
inform their clients if an unsupported interface is queried. The HRESULT is the key
type involved in COM error reporting and is a simple 32-bit value. COM
components use HRESULT to report conditions to their clients. Like other interfaces
in COM, the QueryInterface method returns a HRESULT. The most significant
bit (severity field) of a HRESULT reports whether the function call succeeded or
failed. The last 16 bits contain the code that the function is returning. Two bits are
reserved for future use and the remaining 13 bits provide more information about
the type and the origin of the return code. The following image illustrates this.
So using the following implementation, the component will be able to inform its
clients whether a particular interface is not supported by the component.
/////////////////////////////////////////////////////////////////
//////////
HRESULT __stdcall CComponent::QueryInterface(const IID& iid,
void** ppv)
{
if (iid == IID_IUnknown)
{
Print("Returning pointer to IUnknown...") ;
*ppv = static_cast<IComponent*>(this) ;
}
else if (iid == IID_IComponent)
{
Print("Returning pointer to IComponent interface...") ;
*ppv = static_cast<IComponent*>(this) ;
}
else
{
Print("Interface is not supported!.") ;
*ppv = NULL ;
return E_NOINTERFACE ;
}
return S_OK ;
}
Now the client is able to release the component by calling the component's
Release() method:
//----------------//
// Client
//----------------//
void main()
{
IUnknown* pIUnknown=CreateInstance();
IComponent* pIComponent=NULL;
pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent);
pIComponent->Print("COM from scratch.");
pIComponent->Release();// Releasing the Component
}
The following screen shot is made from the example in part one:
Summary
1. There are many advantages in making applications by components.
2. A common standard should be used for making software components such
that they become language independent, and COM is a binary standard for
making software components.
3. Like hardware components, some sort of functionality are assigned to
software components.
4. The functionality of COM components are obtained through the interface
mechanism.
5. In C++ applications, interfaces are defined as abstract base classes.
6. The memory layout generated by the C++ compiler for a pure abstract base
class is the same as the memory layout required by COM for an interface.
7. As pure abstract base classes have only pure virtual functions and they do not
have instance data, COM components are accessed just through their
methods and never directly through member variables.
8. An interface becomes a COM interface by inheriting from the IUnknown
interface, which has three methods:
o QueryInterface, which is used in order to navigate between multiple
interfaces on an object and returns a pointer to a queried interface.
o AddRef, which is used in order to control the object’s lifetime.
o Release, which is used in order to control the object’s lifetime.
9. In COM, every interface pointer is also an IUnknown interface pointer.
10. A COM component is created by using the new operator.
11. COM components use HRESULT type in order to report conditions to their
clients.
12. Every interface is identified with an interface identifier, which is a 128 bits
value with type of GUID.
13. There is an important difference between a C++ class and a COM class. In
C++, a class is a type, but a COM class is simply a definition of the object,
and carries no type, although a C++ programmer might implement it using a
C++ class.
Introduction
In part one, some background information regarding COM technology was explained,
and a simple example showed how a client can use a component's functionality through
its interface. In this part, I'll guide the reader to separate the implementation of the
component from its client such that the client is no longer bound to the component, and is
able to create it through the class factory.
Software component servers provide a way so that functionality can be reused more
easily, besides they reduce memory overhead when several applications use the same
functionality at the same time, because although each application gets its own copy of the
data, they can share the code. By putting a component into a DLL, it's possible to make a
kind of component distribution and the DLL becomes the component's server and will
contain the implementations of interfaces supported by the component.
Building of the component's Server (DLL)
In the example, the client and the component were both in the same file, now they should
be separated, the client will be in a .exe file, which loads the component into its address
space in order to use it, and the component will be served by a DLL. The client should
load the DLL into its process and create the component before it can get an interface
pointer. If the client links to the CreateInstance() function in the DLL, all other
functions of the component are accessible through an interface pointer. So, the solution is
just to export the CreateInstance() function from the DLL such that the client can link
explicitly to it on the fly. The DLL will be built from the command prompt using one of
Microsoft’s command-line tools.
The following figure shows the Server files which are going to be used in making the
DLL:
Step 1:
Create a source file (Component.cpp) and put the definition and the implementation of
the component's class into it.
Step 2:
Export the CreateInstance() function by adding the following piece of code at the end
of the file:
Step 3:
The linker should be informed that the CreateInstance function will be exported, and
that can be done by using a module-definition file. A module-definition file is a file with
the "def" extension, which contains information about exports, attributes and other
information for linking an .EXE file (which has exports) or DLL. In the .def file, the
CreateInstance function's export ordinal is chosen to be 1. The following part shows
the contents of this file.
;component.def
; Component module-definition file
; LIBRARY Component.dll
DESCRIPTION 'Components windows dynamik library'
EXPORTS ; Explicit exports can go here
CreateInstance @1 PRIVATE
Step 4:
The interface identifier and the interface definition should be known for both the client
and the component, and they can be put into two separate files, which are shared between
the client and the component. Create another source file (GUID.cpp), which can hold the
interface ID:
//
// GUID.cpp - Interface ID
//
#include "objbase.h"
extern "C"
{
Step 5:
//
// Interface.h
//
interface IComponent : IUnknown
{
virtual void __stdcall Print(const char* msg) = 0 ;
} ;
Step 6:
Create a "make" file containing the options for making the DLL with the following
content:
#Makefile
#######################################################################
#########
# Compiler options:
# /c compile without linking
# CL cl.exe is a 32-bit tool that controls the Microsoft C
# and C++ compilers and linker.
# The compilers produce Common Object File Format
# (COFF) object (.obj) files.
# The linker produces executable (.exe) files
# or dynamic-link libraries (DLLs).
#
##################################
# Linker options:
#
# /DEF Passes a module-definition (.def) file to the linker
# /DEBUG Creates debugging information
# /DLL Builds a DLL
LIBS=UUID.lib
#############################################
# Targets:
# CodeProject is just a pseudotarget
#
CodeProject : component
component : Component.dll
#########################################
# Shared source files:
#
GUID.obj : GUID.cpp
Cl $(CPP_FLAGS) GUID.cpp
##########################################
# Component source files:
#
########################################
# Link component:
#
Step 7:
Make the DLL from the Command line by using Microsoft Program Maintenance Utility
(NMAKE.EXE). This program is a tool that can build projects based on commands
contained in a description file.
1. Open the Command window (Click Start choose Run menu item and then
write cmd in the dialog box).
2. From the Command line, change to the directory which contains the server
files.
3. From the Command line, enter: nmake /f makefile.
The NMAKE utility will create the DLL in the same folder:
Building the Client
The following figure shows the files which are used in making the Client. The Client will
be built within the Visual C++ development environment.
Step 1:
Using the AppWizard, create a simple Win32 Console application and choose an empty
project.
Step 2:
Create a new source file (Create.cpp) and make a function which takes the DLL's name
as parameter, loads the "DLL" and then calls the exported function CreateInstance().
The function's return value would be the return value of the CreateInstance() function,
which is an IUnknown interface pointer. In order to link explicitly to the "DLL", the
function calls the GetProcAddress function to get the address of the exported function.
The GetProcAddress function takes two parameters. The first parameter is a handle to
the "DLL" module and the second parameter is the "DLL" name. By calling the
LoadLibrary function, it would be possible to obtain the module handle.
// Create.cpp
#include "iostream.h"
#include "unknwn.h"//IUnknown definition file.
#include "Create.h"
Step 3:
// Create.h
IUnknown* CallCreateInstance(char* dllname) ;
Step 4:
//
// Interface.h
//
interface IComponent : IUnknown
{
virtual void __stdcall Print(const char* msg) = 0 ;
} ;
Step 5:
Create another source file (GUID.cpp), which can hold the interface ID:
// GUID.cpp - Interface ID
#include "objbase.h"
extern "C"
{
extern const IID IID_IComponent =
{ 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3
{ 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4
// The extern is required to allocate memory for C++ constants.
}
Step 6:
Create a source file (Client.cpp) and implement the main function. Call the function made
in step 2, in order to instantiate the component and use its methods:
//--------//
// Client
//--------//
int main()
{
HRESULT hr ;
...
IComponent* pIComponent ;
hr = pIUnknown->QueryInterface(IID_IComponent,
(void**)&pIComponent);
if (SUCCEEDED(hr))
{
...
pIComponent->Print("COM from scratch.") ;
//using the component's functionality
pIComponent->Release() ;
...
}
...
return 0 ;
}
Step 7:
Put the component's server (Component.dll) into the same directory of the client. Now the
client is able to load the DLL into its address space and get the address of the
CreateInstance function by using LoadLibrary and GetProcAddress functions. Build
and run the client program.
The following screen shot shows the client application, after loading the DLL and calling
the component's Print method:
2. In step two from the wizard, choose 'A simple DLL project' and click Finish:
3. Create a new header file and define a class with a member variable and a
member function which can be exported from the DLL:
4. //myclass.h
5.
6. class CMyclass
7. {
8. long m_cRef;
9. public:
10. _declspec(dllexport) void Print(const char*msg);
};
11. Create a source file (myclass.cpp) and implement the member function:
14. Add the DLL.lib file from the DLL project to the Client project (Project->Add to
Project->Files, and then choose Library Files (.lib) as the file type):
15. Copy the DLL into the same folder of the client's executive file. If you build
and run the client application, "COM from scratch." will be written on the
screen, and the DLL will be loaded without any problem.
1. Return to the implementation of the CMyclass class and add a new member
variable:
2. //myclass.h
3. class CMyclass
4. {
5. long m_cRef;
6. int m_i; // a new member variable
7. public:
8. _declspec(dllexport) void Print(const char* msg);
};
10. Rebuild the DLL and copy it into the same folder of the client's executive file.
11. Execute the client application with the new version of the DLL without
rebuilding, and if you run the client application, you will confront with a
problem, and rebuilding of the client application solves this problem.
Rebuilding of client applications is a big problem, because it requires source
codes. Now if you make the same changes to the component's class from the
example in part one and rebuild the DLL and test it with the client application
(without rebuilding it), you will notice that the client runs without any
problem, although a new member variable is added to the component's class
and the implementation of the Print method has been changed. Since
method calls in COM components are indirectly and through their interfaces,
there will not be any problem if the methods are modified.
In the example, although the client and the component have been separated, the client is
closely related to the component's implementation and should know about the DLL's
name, and changing the DLL's name will affect the client. An improvement is that we can
move the component from one DLL to another or to other directories. The solution is to
replace the CallCreateInstance function with a COM Library function called
CoCreateInstance. COM Runtime Library is an integral component of the Windows
operating system, which provides the means for clients to locate and instantiate COM
objects. COM class objects can be identified by CLSIDs (globally unique identifiers),
which are used in order to locate and create an instance of an object. Once the CLSID is
obtained, a client application submits the CLSID to the COM run-time library to load the
COM object and retrieve an interface pointer. Using the CLSID and the registry,
CoCreateInstance locates the specified object, creates an instance of that object, and
returns an interface pointer to that object. In order to use CoCreateInstance to create an
object, the object must be registered with the system.
CoCreateInstance
The COM Library contains this function. The easiest way of creating a component is by
the use of CoCreateInstance function. CoCreateInstance uses a class factory when it
creates a component. It takes a CLSID, creates an instance of the corresponding
component, and returns an interface for this instance of the component.
CoCreateInstance takes 4 in parameters and 1 out parameter (IUnknown*). By passing
an IID to CoCreateInstance, the client doesn't need to call QueryInterface on the
component after creating it.
CoCreateInstance's parameters:
Registration of components
Objects that can be created with CoCreateInstance must also be registered with the
system. Registration maps a CLSID to the automation component file (.dll or .exe) in
which the object resides. If clients will want to obtain CLSIDs at run-time, there must be
a way to dynamically locate and load CLSIDs for accessible objects. Furthermore, there
has to be some system-wide method for the COM Library to associate a given CLSID
(regardless of how the client obtained it) to the server code that implements that class. In
other words, the COM Library requires some persistent store of CLSID-to-server
mappings that it uses to implement its locator services. The COM implementation on
Microsoft Windows uses the Windows system registry as a store for such information. In
that registry, there is a root key called "CLSID" under which servers are responsible to
create entries that point to their modules. Usually, these entries are created at installation
time by the application's setup code, but can be done at run-time if desired. When a server
is installed under Windows, the installation program will create a sub-key under "CLSID"
for each class the server supports, using the standard string representation of the CLSID
as the key name. So the primary entry for a CLSID is a sub-key under CLSID key, which
is the CLSID spelled in hex digits within braces. We may also want to associate a CLSID
with what is called a programmatic identifier or ProgID, which effectively identifies the
same class. A ProgID is a text string without spaces that can be used instead of the
CLSID string. The standard ProgID format is <Vendor>.<Component>.<Version>, such
as Codeproject.Cmpnt1.1. This format is reasonably unique, and if everyone follows it,
there will generally not be a collision. There is also the "VersionIndependentProgID",
which has the same format without the version number. Both the ProgID and the
VersionIndependentProgID can be registered, with a human-readable name as a value,
below the root key. The VersionIndependentProgID is mapped to the ProgID, which is
mapped to the CLSID. To create registry entries, you can either write code or create a
REG file and simply run it to merge its entries with the registry. The following image
shows the registry entries for the Component1 from the Demo Application.
The Class Factory
The CoCreateInstance function does not create COM components directly. Instead, it
creates a component called class factory, which then creates the desired component. A
class factory is a component that creates other components. A particular class factory
creates components that correspond only to a single, specific CLSID. The client uses
interfaces supported by the class factory for controlling how the class factory creates each
component. The standard interface for creating components is IClassFactory interface.
IClassFactory like other COM interfaces is derived from IUnknown interface and has
two methods:
In the following, a class factory is defined in order to create the COM component in the
example:
///////////////////////////////////////////////////////////
//
// Class factory
//
class CFactory : public IClassFactory
{
public:
// IUnknown
virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv)
;
virtual ULONG __stdcall AddRef() ;
virtual ULONG __stdcall Release() ;
// IClassFactory
virtual HRESULT __stdcall CreateInstance(IUnknown* pUnkOuter,
const IID& iid,
void** ppv) ;
virtual HRESULT __stdcall LockServer(BOOL bLock) ;
// Constructor
CFactory() : m_cRef(1) {}
// Destructor
~CFactory() {}
private:
long m_cRef ;
} ;
//
// Class factory IUnknown implementation
//////////////////////////////////////////////////////////////////////
HRESULT __stdcall CFactory::QueryInterface(const IID& iid,LPVOID* ppv)
{
if ((iid == IID_IUnknown) || (iid == IID_IClassFactory))
*ppv = static_cast<IClassFactory*>(this) ;
else
{
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
return S_OK ;
}
///////////////////////////////////
ULONG __stdcall CFactory::AddRef()
{
return ::InterlockedIncrement(&m_cRef) ;
}
////////////////////////////////////
ULONG __stdcall CFactory::Release()
{
if (::InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
//
// IClassFactory implementation
///////////////////////////////////////////////////////////////
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnkOuter,
const IID& iid,void** ppv)
{
HRESULT hr;
if (pUnkOuter != NULL)
{
return CLASS_E_NOAGGREGATION ;
}
CComponent* pComponent = new CComponent ;
if (pComponent == NULL)
{
return E_OUTOFMEMORY ;
}
// Get the requested interface.
hr = pComponent->QueryInterface(iid,(void**) ppv) ;
if(FAILED(hr))
pComponent->Release() ;
return hr ;
}
//---------------------------------------------------------------------
--//
// LockServer
// Called by the client of a class object to keep a server open in
memory,
// allowing instances to be created more quickly.
//---------------------------------------------------------------------
--//
///////////////////////////////////////////////////
HRESULT __stdcall CFactory::LockServer(BOOL bLock)
{
return S_OK ;
}
Before going to more details, it's better to have an overview about creation of the
component via COM Library:
It's also easier to make the DLL within the Visual C++ development environment. In the
following, these steps will be implemented:
Step 1:
• Using the AppWizard, create a new project (with the name "Component") for
the DLL, and select MFC AppWizard (DLL). Consider that the DLL now will
reside in a directory (C:\CodeProject) different form its client:
• In step one, select "MFC Extension DLL" and click the "Finish" button:
• Open the Component.cpp file and replace its content with the following part:
• // Component.cpp : Defines the initialization routines for the
DLL.
• //
•
• #include "stdafx.h"
• #include <afxdllx.h>
• #include "interface.h"
• #include <objbase.h>
• #include "iostream.h"
• #ifdef _DEBUG
• #define new DEBUG_NEW
• #undef THIS_FILE
• static char THIS_FILE[] = __FILE__;
• #endif
• //
• // Component.cpp
•
•
• /////////////////////////////////////////////
• BOOL APIENTRY DllMain(HINSTANCE InsModule,
• DWORD dwReason,
• void* lpReserved)
• {
•
• return TRUE;
}
• Copy and paste the definitions of the component's class and the class factory
and their implementations in the source file Component.cpp, and ignore the
export of the CreateInstance function, because the class factory is going to
create the component.
///////////////////////////////////////////////////////////////////////
//
STDAPI DllGetClassObject(const CLSID& clsid, const IID& iid, void** ppv)
if (clsid != CLSID_Component)
return CLASS_E_CLASSNOTAVAILABLE;
// Create class factory.
CFactory* pFactory = new CFactory ;
if (pFactory == NULL)
return E_OUTOFMEMORY;
// Get requested interface.
HRESULT hr = pFactory->QueryInterface(iid, ppv);
pFactory->Release();
return hr;
Step 3: Registration
{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}
static const GUID CLSID_Component =
{ 0x49bf12f1, 0x5041, 0x48da,
{ 0x9b, 0x44, 0xaa, 0x2f, 0xaa, 0x63, 0xae, 0xfb } };
Create a file with ".reg" extension (component.reg) in order to create registry entries for
the component (using the CLSID):
REGEDIT
HKEY_CLASSES_ROOT\Codeproject.Component.1 =
Codeproject Component Version 1.0
HKEY_CLASSES_ROOT\Codeproject.Component.1\CLSID =
{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}
HKEY_CLASSES_ROOT\Codeproject.Component = Codeproject Component
HKEY_CLASSES_ROOT\Codeproject.Component\CurVer =
Codeproject.Component.1
HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB} =
Codeproject Component 1.0
HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-
AA2FAA63AEFB}\InprocServer32 =
c:\codeproject\component.dll
HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\ProgID
=
Codeproject.Component.1
HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\
VersionIndependentProgID = Codeproject.Component
Activate the registry file by clicking on that. After running the registry file, the entries
will be stored in Windows registry system. The following image shows these entries:
That's it; the chain now is totally broken to pieces. Check it out with the client.
The Client
//-----------//
// Client
//-----------//
void main()
{
HRESULT hr;
IUnknown* pIUnknown;
IComponent* pIComponent;
IClassFactory* pIClassFactory;
::CoInitialize(NULL);
/*
//Once the CoCreateInstance is called, the component
//will be created and the client can not
//control it, that's why CoCreateInstance is inflexible
//and the solution is to call CoGetClassObject function
hr = ::CoCreateInstance(CLSID_Component,NULL,
CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pIUnknown) ;
if (SUCCEEDED(hr))
{
hr=pIUnknown-
>QueryInterface(IID_IComponent,(void**)&pIComponent);
if(SUCCEEDED(hr))
pIComponent->Print("COM from scratch.");
}
*/
//-------------------------------//
// improvement of the client code
//------------------------------//
// By calling the CoGetClassObject function, the client can control
// creation of the component
hr=CoGetClassObject(CLSID_Component,CLSCTX_INPROC_SERVER,
NULL,IID_IClassFactory,(void**)&pIClassFactory);
if (SUCCEEDED(hr))
{
hr=pIClassFactory->CreateInstance(NULL,
IID_IComponent,(void**)&pIComponent);
if(SUCCEEDED(hr))
pIComponent->Print("COM from scratch.");
}
::CoUninitialize ();
}
Introduction
In part one, some background information regarding to the COM technology was
explained, and a COM component was made by a simple example. In part two, the code
in the example was optimized, such that the component was no longer bound to its client,
and it could be created via COM Library. This part is about the containment mechanism,
and I'll guide the reader to reuse the component made in part two in another component.
COM provides two mechanisms for code reuse. These mechanisms are called
containment or delegation and aggregation. In the containment mechanism, one object
(the “outer” object) becomes the client of another, internally using the second object (the
“inner” object) as a provider of services that the outer object finds useful in its own
implementation. COM containment is similar to C++ containment; however, it is at the
interface level. The outer component contains pointers to interfaces on the inner
component. The outer component implements its own interfaces using the interfaces of
the inner component and it can also re-implement the inner component's interfaces by
forwarding calls to the inner component and adding code before and after the code for the
inner component. Containment reuses the implementation of the interfaces belonging to
the inner components. One of the components (CComponent3) from the demo application
(illustrated in the above figure), uses this mechanism. Until now, a component has been
created and its interface supports the Print method. Now, in order to use the containment
mechanism, we can use this interface in a new component. In the following, a new
component will be created, which reuses the previous component’s interface in order to
build its own interface. This new interface will support two new mathematical functions
and will be called IMath interface.
Using the same method in part two, create en empty project in order to make the
component's server(Component2.dll). The steps for creation of the DLL are explained in
details in part two.
As you may know, every COM interface should be derived directly or indirectly
from the IUnknown interface, so the new interface may be derived form the
interface which was implemented in the pervious parts, and that interface was
derived directly from the IUnknown interface:
};
As you may know, the component's class can be defined by deriving a class from
the newly created COM interface, and because the containment mechanism will
be used, there is a need for a method to create the inner component (which can be
called from the class factory), and a pointer to the inner interface:
public:
//IUnknown
virtual HRESULT __stdcall QueryInterface(const IID & iid,void**
ppv);
virtual ULONG __stdcall AddRef();
virtual ULONG __stdcall Release();
//IComponent (interface of the inner component)
virtual void __stdcall Print (const char* msg);
//IMath
virtual int __stdcall Sum(int a,int b){return(a+b);}
virtual int __stdcall Subtract(int a,int b){return(a-b);}
// A method to create the inner component
HRESULT __stdcall CreateInnerComponenet();
//Constructor
CComponent2();
//Destructor
~CComponent2();
};
• Implementation of CComponent2::CreateInnerComponent:
• Implementation of CFactory::CreateInstance():
• /////////////////////////////////////////////////////////////////
///////
• HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnkOuter,
• const IID& iid,void**
ppv)
•
• {
• // Cannot aggregate
• if (pUnkOuter != NULL)
• return CLASS_E_NOAGGREGATION ;
•
• // Create component2(the outer component).
• CComponent2* pComponent2 = new CComponent2 ;
•
• if (pComponent2 == NULL)
• return E_OUTOFMEMORY ;
•
• // Create the inner component.
• HRESULT hr = pComponent2->CreateInnerComponent() ;
• if (FAILED(hr))
• {
• pComponent2->Release() ;
• return hr ;
• }
•
• // Get the requested interface.
• hr = pComponent2->QueryInterface(iid,(void**) ppv) ;
• //if Query faild the component will delete itself
•
• if (FAILED(hr))
• pComponent2->Release() ;
•
• return hr ;
}
• Providing GUID for the component's class and interface identifier for IMath
interface, using GUIDGEN.EXE:
• //CLSID for component2's class
• // {0CDD7D97-DD93-450a-BBCF-6B22894FAFF5}
• extern "C" const GUID CLSID_Component2 =
• { 0xcdd7d97, 0xdd93, 0x450a,
• { 0xbb, 0xcf, 0x6b, 0x22, 0x89, 0x4f, 0xaf, 0xf5 } };
•
• //IID for component2's IMath interface
• // {C8ACE8CC-0480-4f1a-8A62-89717E1D5705}
• extern "C" const IID IID_IMath =
• { 0xc8ace8cc, 0x480, 0x4f1a,
{ 0x8a, 0x62, 0x89, 0x71, 0x7e, 0x1d, 0x57, 0x5 } };
Step 2: the Client
The following figure shows the output window of the client program:
One of the uses of containment is to extend an interface by adding code to an existing
interface. As an example, the inner Component2 from demo application has a window
with yellow color, although Component2's background color is initially white. The other
change is that the butterfly starts flying after creation of the inner component, although
initially it does not fly. These changes are made through the containment mechanism. The
following piece of code and figure show how one of the methods of Component2 has
been specialized.
/////////////////////////////////////////////////////////////////////
void __stdcall CComponent3::ShowWndBackground_Com2(COLORREF bgcolor)
// default background color is white
{
//---------------------------------------------------------------------
-----------//
//The outer component (Component3) can reimplement an interface
supported
//by the inner component by forwarding calls to the inner component.The
outer
//component can specialize the interface by adding code before and
after the code
//for the inner component. As an example the background color of
component2's window
//will be changed to yellow
//---------------------------------------------------------------------
-----------//
bgcolor=RGB(255,255,0); //bgcolor is changed to yellow color
//---------------------------------------------------------------------
-----------//
m_pIComponent2->StartFlying();//The butterfly should fly
m_pIComponent2->ShowWndBackground_Com2(bgcolor);
}
The demo application's code is very similar to the example explained in the article and
only a window is used to visualize each component, and each window's menu acts as the
component's interface. I hope that these articles were useful for you and could be a guide
to begin with "COM" from scratch.
Introduction
Some programmers think DLLs are complicated but it's true they are like EXEs.
However I have seen programmers who can't build DLL. There are multiple way to
create a DLL. For usage with exe, easiest way is "Implicit linking". In this way, we
require .LIB file produced by DevStudio. Below, we create a DLL and then use it with
exe. Inside DLL, we calculate (multiply) two numbers that has been send from the exe.
Practically, exe is "Sender/Receiver" and DLL is "Calculator".
1. Run VC++.
2. Choose : File > New.
3. Create "MFC AppWizard (DLL)" (named e.g. : MyFirstDll).
4. Declare in top of file:
5. //
6. #define DLLEXPORT __declspec(dllexport)
7. //
__declspec(dllexport)'s purpose is to add the " export directive " to the object
file. So you don't need a .DEF file. To make code more readable, define a macro
for __declspec(dllexport) : DLLEXPORT.
8. Now for exporting per function that you want, type before it : "DLLEXPORT" like
this:
9. DLLEXPORT int Multiply(int ParOne,int ParTwo)
10.{
11. int Mlt=ParOne*ParTwo;
12. return Mlt;
}
13. Press Build button.
14. Bring out DLL from oven!!
Note that linker also builds an " import library " with the same DLL name but
with .lib extension.
1. Run VC++.
2. Choose : File > New.
3. Create "MFC AppWizard (exe)".
4. Choose "Dialog based".
5. Choose : Project > Add To Project > New > C/C++ Header File.
6. Name file e.g. : Imports.
7. Declare in Imports.h:
8. //
9. #define DLLIMPORT __declspec(dllimport)
//
__declspec(dllimport)'s purpose is to add the " import directive " to the object
file. To make code more readable, define a macro for __declspec(dllimport) :
DLLIMPORT.
This is the same function that you defined in DLL. Here, it has been introduced as
an "import function". But it is unrecognized for current project and we must
resolve it for linker.
11. Copy .lib file from release or debug folder (depends current project setting) in
previous project to current directory project because it must link with exe. Inside
.lib file (same import library), exists information about exported functions of
DLL.
12. Back to VC++ environment and Choose : Project > Settings > Link (tab).
13. Type [.lib file previous project] in "Object/library modules:". For example,
MyFirstDll.lib. Then press OK. It will resolve externals. Here, "int
Multiply(int ParOne,int ParTwo);" function.
14. Because we intend to use function in ...Dlg.cpp file (e.g. SimpleDllDlg.cpp), type
in top of file:
15.//
16.#include "Imports.h"
//
17. Now you can use exported function of DLL like this:
18.void CDllFunDlg::OnMltply()
19.{
20. UpdateData(TRUE);
21. m_Result=Multiply(m_ParamOne,m_ParamTwo);
22. UpdateData(FALSE);
}
Introduction
In my previous article, I mentioned simply DLL creation and using it. But it exports
function only. Now, I want to describe "how to export classes from a DLL?"
1. Run VC++.
2. Choose : File > New.
3. Create "MFC AppWizard (DLL)" (named e.g.: MyScndDll).
4. Click right mouse button on root branch in "ClassView", point and click "New
Class...".
5. Select Generic Class as class type, and name it e.g. CRectArea.
6. Declare in top of file of "RectArea.h":
7. //
8. #define DLLEXPORT __declspec(dllexport)
//
9. Now, for exporting per class that you want, type before its declaration:
"DLLEXPORT" like this:
10.class DLLEXPORT CRectArea
11.{
12. public:
13. CRectArea();
14. virtual ~CRectArea();
};
15. Now, declare a member function for newly created class. E.g., Calculate:
16.class DLLEXPORT CRectArea
17.{
18.public:
19. int Calculate(int ParOne,int ParTwo);
20. CRectArea();
21. virtual ~CRectArea();
};
Note: linker also builds an "import library" with same DLL name but .lib extension.
1. Run VC++.
2. Choose : File > New.
3. Create "MFC AppWizard (exe)".
4. Choose "Dialog based", then click Finish.
5. Choose: Project > Add To Project > New > C/C++ Header File.
6. Name file, e.g.: Imports.
7. Declare in Imports.h:
8. //
9. #define DLLIMPORT __declspec(dllimport)
//
10. Type after it, declaration of exported function from DLL. Note: we must type
"DLLIMPORT" whenever intending to import class:
11.class DLLIMPORT CRectArea
12.{
13. public:
14. int Calculate(int ParOne,int ParTwo);
15. CRectArea();
16. virtual ~CRectArea();
};
This is the same class that you declared and defined in the DLL. Here, have
introduce as "import class". But it is unrecognized for current project and we must
resolve it for linker.
17. Copy .lib file from release or debug folder (depending on current project setting)
in previous project to current directory project because it must link with EXE.
Inside .lib file (same import library), exist information about exported class from
DLL.
18. Back to VC++ environment and choose: Project > Settings > Link (tab).
19. Type [.lib file previous project] in "Object/library modules:"
For example: MyScndDll.lib, then press OK. It will resolve externals. Here:
CRectArea class.
20. Cause we intend to use function in ...Dlg.cpp file (e.g.: ImportClassDlg.cpp), type
in top of file:
21.//
22.#include "Imports.h"
//
23. Now, you can use function of exported class, like this:
24.void CImportClassDlg::OnCalc()
25.{
26. UpdateData();
27. m_Result=m_RectArea.Calculate(m_ParOne,m_ParTwo);
28. UpdateData(FALSE);
}
Introduction
In third part of assortment articles "DLLs are Simple!", I describe how to create a DLL
using a .DEF file and using it.
What is .DEF file? It is a module-definition (.DEF) file that is a text file containing one
or more module statements that describe various attributes of a DLL include:
1. Run VC++.
2. Choose File>New.
3. In the dialog box, choose "MFC AppWizard (DLL)" and name it (e.g.
"DefExported")
4. Declare a member function:
5. public:
6. int Multiply(int PartOne,int PartTwo);
7. CDefExportedApp();
8.
9. Then define it:
10.int CDefExportedApp::Multiply(int PartOne, int PartTwo)
11.{
12. return PartOne*PartTwo;
13.}
14.
15. In the FileView tab, click "Source Files" and double click on "DefExported.def."
After the EXPORT statement, enter "[function name] @[number]" like this:
16.LIBRARY "DefExported"
17.DESCRIPTION 'DefExported For Present in CodeProject'
18.EXPORTS
19. ; Explicit exports can go here
20. Multiply @1
21.
22. Click Build Button.
23. Bring out DLL out of the oven!!
1. Run VC++.
2. Choose from menu File>New.
3. In the dialog box, choose "MFC AppWizard (EXE)" and name it (e.g.
"DynamicLoad")
4. Select "Dialog Based," and click the Finish button.
5. Place a button control on the dialog and double click on it for create its click
event.
6. Before typing the code for the button click (BN_CLICKED) event, we must
define a new function pointer with the correct number of parameters,
according to the parameters of the function we exported above.
typedef int (CALLBACK* LPFNMLTPLY)(int,int);
Sometimes you have to convert some variable types. For more information about
this conversion see Microsoft Support Article ID: Q117428.
FreeLibrary( hClcltr );
Introduction
As you know, creating multimedia applications with special effect like sound, animation,
web-link and so on is time-consuming. But there is a method that is rapid and engaging.
A resource-only DLL is a DLL that contains nothing but resources, such as icons,
bitmaps, strings, and dialog boxes.
What is necessary?
Note: In your HTML file, create two buttons with links "exit" and "execute" like below :
<p><a hidefocuse href="execute">
<img border="0" src="ExecNorm.gif" ALT="This may execute somethings"
style="cursor: hand; position: absolute; z-index: 1; left: 45; top: 120"
onmouseover="this.src='ExecHot.gif'"
onmouseout="this.src='ExecNorm.gif'"
width="75" height="43" >
</a></p>
1. Run VC++.
2. Click New on the File menu and then choose MFC AppWizard (dll); name it
"ResOnly"
3. Click finish button in next step.
4. In ResourceView tab right-click on the root branch.
5. Choose the Import... command.
6. Import your HTML file; note only the HTML file.
7. Click Save All on the File menu and then close VC++.
8. Go to the folder that contains HTML file and all its needed files and copy all
files to VC++ project folder (ResOnly).
9. In ResOnly project folder find .rc file and open it with notepad; point to HTML
section. Now convert:
10."IDR_HTML1 HTML DISCARDABLE "Skin.htm"
11.TO :
"Skin.htm HTML DISCARDABLE "Skin.htm"
Then add all needed files for imported HTML just below it:
/////////////////////
Skin.htm HTML DISCARDABLE "Skin.htm"
BG.gif HTML DISCARDABLE "BG.gif"
c.gif HTML DISCARDABLE "c.gif"
ExecHot.gif HTML DISCARDABLE "ExecHot.gif"
ExecNorm.gif HTML DISCARDABLE "ExecNorm.gif"
ExitHot.gif HTML DISCARDABLE "ExitHot.gif"
ExitNorm.gif HTML DISCARDABLE "ExitNorm.gif"
WMPAUD7.WAV HTML DISCARDABLE "WMPAUD7.WAV"
pupil.gif HTML DISCARDABLE "pupil.gif"
whites.gif HTML DISCARDABLE "whites.gif"
////////////////////
12. Save and close notepad and open VC++ Project (ResOnly). Press Build
button. Now you have a DLL containing resources; in fact, it is the "Resource-
Only DLL".
1. Run VC++.
2. Choose New on the File menu and then choose MFC AppWizard (exe).
3. In step1 dialogbox, select Single document and press next button till
step6. Now you choose CHtmlView as Base Class; then press Finish button.
4. Open CHtmlView derived class; in my project "CShowcaseView".
5. In OnInitialUpdate() member function, instead of default parameter of
Navigate2 type give:
e.g. : res://ResOnly.dll//Skin.htm
What is OnInitialUpdate()?
This member function is called by the framework after the view is first attached to
the document, but before the view is initially displayed. Here you call Navigate2
function and force view for representing Skin.htm file. By "res:", you can refer to
an HTML page embedded in the resources of a dynamic-link library (.dll) file. It
is a protocol like "http:".
It is clear, it tests URL. If the last segment was "exit", it would have terminated program;
but it was "execute"; it does something that you want.
Now it is ready. If you act correctly, that must be like my demo exe file.
Introduction
Based on DelayLoadProfileDLL.CPP, by Matt Pietrek for MSJ February 2000. This code
is intended to be included in a DLL inserted through a global Windows Hook (CBT hook
for example). It will replace functions from other DLLs (e.g. DDRAW.DLL) with
functions from your DLL.
SDLLHook D3DHook =
{
"DDRAW.DLL",
false, NULL, // Default hook disabled, NULL function pointer.
{
{ "DirectDrawCreate", MyDirectDrawCreate },
{ NULL, NULL }
}
};
return TRUE;
}
Now all that remains is to get your DLL loaded into the target process.
•
Introduction
This article shows how to add a menu interface to an application from a DLL at any time.
This example was written using VC++.NET 2003 but it should translate to VC++ 6.0
easily, as much of this was developed using 6.0 early on. No managed code was harmed
in the process of writing this article.
Background
I wanted a flexible way to load a DLL into my application to test it in-house and not
leave any marks when the customer got the application. Future enhancements could
include targeted computer-based training modules.
This is not meant to be a be-all end-all treatise, but, rather, a springboard for extending
applications after the application has been written.
There are two parts to this problem: the plug-in DLL and the target application. The
target application needs to know how to call the plug-in DLL without the benefit of lib
file. We accomplish this by standardizing the plug-in interface. The interface for this
project is in the TestPlugin project in plugin_api.h. There are other ways of handling the
interface. For instance, you could create a structure of function pointers and populate it
after LoadLibrary and clean it out before FreeLibrary. In this example, you only have
to store the HMODULE value. If you had more than one plug-in DLL, you would only need
to store the HMODULE values and not have to carry many structures of the function
pointers.
The Interface: There are four methods defined publicly for this DLL in TestPlugin.def.
They are InstallExtMenu and RemoveExtMenu which install and remove the menus
respectively, GetExtMenuItemCount which gives the application the number of menu
items installed, and GetExtMenuItem which serves to map the menu control ID to a
Windows message identifier. More can be done to extend the interface but this appears to
be the bare minimum to get this up and running. The file plugin_api.h handles the details
of connecting to the correct DLL based on a save HMODULE value from LoadLibrary.
CCommandWnd: This window receives the registered message from the target application.
When the application initializes the plug-in DLL, it passes an HWND to the DLL so that the
DLL can set up the menus for that window. In addition to setting up the menus, the DLL
also creates a CCommandWnd window as a child to the window passed in.
Points of Interest
I have set up a macro called _PLUGIN_ON_DEMAND in stdafx.h. If you undef this macro, the
CTargetApp will try to load the DLL in its InitInstance method and unload it in the
ExitInstance method. I have also included an alternate IDR_MAINFRAME menu (with the
subtitle ALTMENU) that you can use when the _PLUGIN_ON_DEMAND is not defined to
show how the menus can be added when the top-level Tests menu does not exist.
History
This is version 1.0!
Introduction
DLLs are a great way of sharing common pieces of code data between applications.
When it comes down to exporting C++ classes from DLLs most of us go for MFC
extension DLLs where we can use the AFX_EXT_CLASS macro to export an entire class.
Unfortunately, MFC is no lean and mean class architecture, which means that distributing
MFC extension DLLs mean that you have to include the big MFC runtime not to mention
the fact that your DLL can only be linked to MFC applications exclusively. What's the
solution then? Enter standard Win32 DLLs.
Details
I couldn't believe my eyes on how easily one can export C++ classes directly from a plain
vanilla Win32 DLL. Just make one and insert your classes into the DLL. Now simply put
__declspec(dllexport) in between the class keyword and the class name, i.e.
// in your header...
public:
void SayHello();
};
// in your cpp...
void CDllTest::SayHello()
{
printf(_T("Hello C++"));
}
That's it! The sample code and project are pretty self explanatory. Enjoy.
Introduction
This article shows a step by step technique to create your first DLL with VC++.
• Create a Win32 Dynamic Link Library project and add a .cpp & a .h file.
• In the .cpp file, create a class instantiated from the CWinApp file.
•
• # include <stdafx.h>
• # include "SourceFile.h"
•
• class CDllApp : public CWinApp
• {
• public:
•
• CDllApp::CDllApp()
• {
• Calc(0,0);
• }
•
• DECLARE_MESSAGE_MAP()
• };
•
• BEGIN_MESSAGE_MAP(CDllApp,CWinApp)
•
• END_MESSAGE_MAP()
•
CDllApp DllObject;
• In the Link tab of the "Project->Settings" Dialog, Go to the Text Box labeled
"Object / Library Modules" and specify the path of the Dll File. Then copy the
compiled dll file to your current app path directory and run the program.
The DLL file may not be visible due to the File View Options in the Windows folder. So,
U can either go to the DOS Prompt and copy the file or enable the setting "Show all files"
in Windows explorer to copy the file. To Create a DLL that uses MFC, See the following
example. Note that extern "C" has not been used and the macro
AFX_MANAGE_STATE(AfxGetStaticModuleState()); has been used to implement MFC.
What is a DLL? DLL stands for Dynamic Link Library. Using DLL's offers several
advantages as mentioned below:
// CMyClass.h
//{----------- Code Snippet 1 --------------------------
class CMyClass
{
public:
__declspec(dllexport) CString SayHello (CString strName);
__declspec(dllexport) CMyClass();
__declspec(dllexport) virtual ~CMyClass();
};
Now we write a 'Client' to test our DLL This is a MFC dialog based application with a
edit box and two buttons.
1. Select New MFC AppWizard(exe) from the project menu and type in the
project name TestDLL
2. Choose Dialog based application and click Finish
3. You would now be looking at the application in the resource view. Add an edit
box to the application by selecting the edit box, next click on the dialog box
and drag.
4. Also create a CString (value) variable associated with it, call this variable
m_edit i.e Click on the edit box and press CTRL and W to bring up the class
wizard. Choose the member variables tab and choose IDC_EDIT1 and click on
“Add Variable”, of type "Value" and type m_edit
5. Now a file called TestDLLDlg.cpp would have been generated .
6. Double click the “Ok” button on the dialog, the wizard pops up a box asking a
name for the function, choose the default name “OnOk” to go to the
TestDLLDlg.cpp file
7. Modify TestDLLdlg.cpp to look like the Code Snippet 1, the code entered has
no effect and the project at this point will not compile. (Basically we are
calling a method of a class object, this object has not been defined as yet.)
8. Goto the file TestDLLDlg.h and include the header file for your class i.e
MyClass.h .
9. In the file TestDLLDlg.h declare an object of your class objMyClass present
in the DLL
10. After steps 8 & 9 the code looks as in Code Snippet 2
11. We need to modify the project settings to compile the project, Click on
"Project->Settings->Link" and in the "Object/Library Modules" enter the
complete or relative path to the DLL's library file. i.e. A .lib file is generated in
the same folder as your DLL, I have entered "..\MyDll\Debug\MyDll.lib" here.
12. Compile the application and if it has compiled successfully. Run it.
Hey, Why are we getting this stupid box saying "Unable to located DLL" ?
Solution: Copy the MyDll.dll to the same folder as the TestDll.exe
Run the application, enter a name in the text box and click the okay button, this will show
a message box containing the same text prefixed with a hello.
//TestDLLdlg.cpp
// ----------------- Code Snippet 1 ----------------------------
void CTestDLLDlg::OnOK()
{
UpdateData(true);
CString strResult = objMyClass.SayHello(m_edit);
AfxMessageBox (strResult);
//CDialog::OnOK();
}
// ----------------- Code Snippet 1 ----------------------------
// ----------------- Code Snippet 2 ----------------------------
//TestDLLDlg.h
///////////////////////////////////////////////////////////////////////
//////
// CTestDLLDlg dialog
#include "..\MyDll\MyClass.h"
// Dialog Data
I'm interested in Hi-Performance code, OO Methodology, OS API's, Optimizing Windows, Tweaking IE.
Introduction
This is a easy to use library which let you use different compilers, to optimize-compile
your DLL for different processors.
Details
Move all the to be optimized classes and functions to a DLL, and the DelayLoader library
will automatically import all the functions from the fastest (for this processor available)
DLL.
• DelayDLL, a library project which is the one to use in your own workspaces
• MainDLL, a sample DLL which exports a function
• Test, a sample DLL which exports a function
• MainMFC, a sample project which makes uses of the projects above, no fancy
MessageBox, just step through with debugger to see which DLL is loaded. A
TRACE output is done, so you can easily see which DLL is currently loaded
The library is based on the Delay loading of DLL. So we start loading the DLL when it is
needed for the first time. No LoadLibrary and GetProcAdress functions need to be
called, The Delay Loading from Microsoft takes care of this.
With a specific hooking mechanism we can force our app to load a different DLL than
compile-time was programmed. We could even get the adress of a different named
function. The DelayDLL.dll should be loaded by the MainMFC program, but because we
run this program on my Athlon MP the DelayDLL.Athlon.dll file is loaded.
This hook makes it very easy when we build a program which should make use of
processor specific optimizations. The UnloadDelayedDLL( LPSTR ) method can unload
a DLL at runtime, to free up memory, for if you don't have 1Gb of RAM like me :-p
How to use
You could only add a copy of Win32 Release, and call it w32 Athlon Rls. Or also make a
copy of the win32 Debug, and call it w32 Athlon Dbg. The only thing you have to change
in the project settings, is on the link tab, the Output name, to MainDLL.Athlon.dll for
example.
And of course you should select another compiler, for this configuration ( using the
Codeplay Compiler Controller Plug-in ). Or make some assembler files yourself and use
them for this configuration. For now only compile the .c files using VectorC, so not the
complete project. ( no cpp support yet ). Of course you can also use the Intel compiler to
create optimized DLLs.
Here is an example of file to be added to your project, which will make use of the
optimized DLLs.
/*****************************************/
/*Which DLLs should be Delay loaded */
/***************************************/
// Use Upper and Lower case letters, exactly the same as in de DLL
// Project ,Thanks to MS DelayLoader :-(
#pragma comment(linker, "/DelayLoad:Test.dll")
#pragma comment(linker, "/DelayLoad:MainDLL.dll")
DLLDescriptor TestDLL[] =
{
1,NULL,"Pentium.dll",DELAYDLL_PROC_PENTIUM,DELAYDLL_COMP_INTEL,
NULL
};
DLLCollection pDelayLoadedDLLs[] =
{
"Test.dll", TestDLL, // Tries to load first : Test.Pentium.dll,
then Test.dll
"Test2.dll", TestDLL, // Tries to load first : Test2.Pentium.dll,
then Test2.dll
"MainDLL.dll", MainDLL,// Tries to load first : MainPIV.dll, then
MainDLL.Athlon.dll,
// then MainDLL.PII.dll, then MainDLL.dll
NULL
};
//<END OF FILE>
About ETA
Introduction
The readers/writers problem is one of the classic synchronization problems. Like the
dining philosophers, it is often used to compare and contrast synchronization
mechanisms. It is also an eminently practical problem.
Two kinds of processes -- readers and writers -- share a database. Readers execute
transactions that examine database records; writer transactions both examine and update
the database. The database is assumed initially to be in a consistent state (i.e., one in
which relations between data are meaningful). Each transaction, if executed in isolation,
transforms the database from one consistent state to another. To preclude interference
between transactions, a writer process must have exclusive access to the database.
Assuming no writer is accessing the database, any number of readers may concurrently
execute transactions.
Some time ago at work, we had to implement a server that relays and translates his
incoming datafeed to multiple (typically > 32) clients. As this datafeed represents the
continuously (but on a non-regular time base) changing (stock/option) market prices, fast
relaying is crucial. We developed a solution that consists of one receiving thread, multiple
translator threads, and even more sending threads (since we do not want to block the
server if a client fails to receive).
Obviously, all the threads need access to the received and / or translated packet. To
achieve this without corrupting data, synchronization is necessary. Searching the MSDN,
resulted in finding several synchronization objects (CCriticalSection, CMutex, etc.) of
which none seem to fulfill our demands: Multiple read-access when not writing. We thus
decided to write the desired synchronization object ourselves: CMutexRW.
Since our problem has extensively been studied (since 1960?) we first turned to
some old college-books on parallel formal semantics to refresh our knowledge about
the problem. Soon we found the pages describing the readers and writers problem
and (several) solution outlines. We chose to implement our solution (with readers
preference) using semaphores.
First some quick (probably skipable) refresh course to (formal) semaphores: Semaphores
where first introduced by Dijkstra in 1968, who thought it to be an useful tool for
implementing mutual exclusion and for signalling the occurrence of events such as
interrupts. A semaphore is an instance of an abstract data type: it has a representation that
is manipulated only by two special operations, P and V. Because Dijkstra is Dutch, the P
and V stand for Dutch words. In particular, P is the first letter of the Dutch word
passeren, which means `to pass'; V is the first letter of vrijgeven, which means `to
release'. The V operation signals the occurrence of an event; the P operation is used to
delay a process until an event has occurred. In particular, the two operations must be
implemented so that they preserve the following property for every semaphore in a
program.
By using this definition of semaphores the readers and writers problem can be solved in
the following way:
integer nReaders := 0
semaphore semReaders := 1
semaphore semWriters := 1
Reader[i: 1 .. m] :: do true ->
P( semReaders )
nReaders := nReaders + 1
if nReaders = 1 -> P( semWriters ) fi
V( semReaders )
P( semReaders )
nReaders := nReaders - 1
if nReaders = 0 -> V( semWriters ) fi
V( semReaders )
od
Writer[j: 1 .. n] :: do true ->
P( semWriters )
write the database
V( semWriters )
od
Clearly this solution gives readers preference over writers; once one reader is accessing
the database, all readers are allowed to read the database without performing the P or V
operations on semWriters.
virtual ~CMutexRW()
{
if (m_semWriters)
VERIFY( ::CloseHandle(m_semWriters) );
m_semWriters = NULL;
if (m_semReaders)
VERIFY( ::CloseHandle(m_semReaders) );
m_semReaders = NULL;
}
// P( semReaders )
dwEvent = ::WaitForSingleObject( m_semReaders, INFINITE );
ASSERT(dwEvent == WAIT_OBJECT_0);
m_nReaders++;
if (m_nReaders == 1)
{
// P( semWriters )
dwEvent = ::WaitForSingleObject( m_semWriters, INFINITE );
ASSERT(dwEvent == WAIT_OBJECT_0);
}
// V( semReaders )
VERIFY( ::ReleaseSemaphore( m_semReaders, 1, NULL ) );
};
m_nReaders--;
if (m_nReaders == 0)
{
// V( semWriters )
VERIFY( ::ReleaseSemaphore(m_semWriters, 1, NULL) );
}
// V( semReaders )
VERIFY( ::ReleaseSemaphore( m_semReaders, 1, NULL ) );
};
// P( semWriters )
dwEvent = ::WaitForSingleObject( m_semWriters, INFINITE );
ASSERT(dwEvent == WAIT_OBJECT_0);
}
};
Of course we would also like to have some objects behaving like MFC's CSingleLock,
i.e. automatically unlocking the locked CMutexRW upon leaving scope. Here's my
implementation of CReadLock:
class CReadLock
{
protected:
CMutexRW* m_pMutexRW;
bool m_bIsLocked;
public:
CReadLock(CMutexRW* pMutexRW, const bool bInitialLock = false) :
m_pMutexRW(pMutexRW), m_bIsLocked(false)
{
ASSERT(m_pMutexRW);
if (bInitialLock){
m_pMutexRW->Lock_DataRead();
m_bIsLocked = true;
}
};
inline const bool& IsLocked() const{
return m_bIsLocked;
};
class CWriteLock
{
protected:
CMutexRW* m_pMutexRW;
bool m_bIsLocked;
public:
CWriteLock(CMutexRW* pMutexRW, const bool bInitialLock = false) :
m_pMutexRW(pMutexRW), m_bIsLocked(false)
{
ASSERT(m_pMutexRW);
if (bInitialLock){
m_pMutexRW->Lock_DataWrite();
m_bIsLocked = true;
}
};
Usage example
Using these objects is quite straightforward; when wanting to read the data guarded
by a CMutexRW object, construct a CReadLock object in the local scope, obtain the
lock and do the actual read. Next, release the lock, or leave the local-scope. In the
same way we can write the data in a thread-safe way.
class MyExampleObject{
protected:
CMutexRW m_mutexRW;
int m_Data;
public:
void Set(const int& srcData){
CWriteLock writeLock(&m_mutexRW, true);
m_Data = srcData;
}
void Get(int& destData){
CReadLock readLock(&m_mutexRW, true);
destData = m_Data;
}
};
There is (as observable) a small annoying problem; the Get routine is not const, i.e. the
MyExampleObject does not behave as proper (data) objects should. This can be easily
fixed by implementing the code as follows:
class MyExampleObject{
protected:
mutable CMutexRW m_mutexRW;
int m_Data;
public:
void Set(const int& srcData){
CWriteLock writeLock(&m_mutexRW, true);
m_Data = srcData;
}
void Get(int& destData) const{
CReadLock readLock(&m_mutexRW, true);
destData = m_Data;
}
};
Standard disclaimer
Although we use the discussed objects (slightly modified for better performance)
extensively in our software, I cannot guarantee a 100% bug-proof, thread-safe and
so on behaviour. So use it, use it well, but never forget, I'm just another programmer
;).
About Joris Koster
Joris lives in the geographical center of The Netherlands, Utrecht, where he started his academic
education Computational Science in 1995.
In (aug) 2002 he (finally ) obtained his master's degree by finishing his Master's thesis resulting from
research into parallel matrix-vector multiplication on supercomputers.
In 1997 he started working (parttime) at Accent Pointe, the IT department of Accent Groupe, a market
maker company at the Amsterdam Euronext Exchange.
From aug 2002 he continued working at Accent Pointe, but on a full-time basis, where his major
occupation became the development of a new portfolio management model (like Black & Scholes,
Binominal trees, etc.) and a fast (parallel) implementation.
Besides this major occupation, several minor engineering projects ensure his programming skills are
kept up to date
Abstract
I am going to list results of my own investigation and also refer to other investigators’
results. At the end of the article, I will give my approach for solving one of the ‘DLL hell’
problems.
Introduction
Once I’ve got a task to solve the problem with the DLL updates. Some company supplied
users with DLLs that contained classes that developers used in their C++ applications. In
other words, it was some kind of a SDK in the DLL. There were no restrictions on how to
use these classes, so developers derived from them while creating their applications. Soon
they realized that their applications started to crash after the DLL updates. Developers
were required to rebuild their applications to get versions that can work with the new
DLL.
Further I will provide you with my research results on this problem, with the information
I have gathered from the external sources and finally I will give my solution for one of
the ‘DLL Hell’ problems.
Research results
As I have understood, the issue was in the modification of the base classes placed in the
supplied DLL. I’ve overviewed several articles and found that the problem of the DLL
backwards compatibility is not a new one but as a true researcher I’ve decided to make
several experiments personally. As a result, I have got the following list of problems:
1. Adding of a new virtual method to a class exported from a DLL can cause the
following problems:
o If class already has a virtual method B and we are adding a new one
named A before it, then we are changing the table of virtual methods.
Now the first virtual method in the table will be A but not B and the
client program which calls B will fail without recompilation as the call of
B will cause the call of A and this is another method which possibly has
other parameters and return type.
o When a class doesn’t have virtual methods and none of its base
classes have, then adding of a new virtual method to it or to its base
class will cause adding of the pointer for the table of virtual methods.
This will cause change in the class size. So the error will occur after a
client program will allocate memory for the class (an amount of
memory that was required before the class has been changed) and will
try to modify some of the class' fields explicitly or implicitly. In this
case, as the pointer to the table of virtual method is added to the
beginning of the class, all addresses of the class fields are shifted and
thus we will get an abnormal client program behavior or a runtime
error.
o In case when a class has virtual methods or any of its parent classes
has, we can’t add virtual methods to classes exported from the DLL if
they participate in the inheritance. We can’t add virtual methods not
only to the beginning of the class declaration but also to the end of it.
The problem is in shifting in the table of virtual methods. Note that
even if you add a new virtual method to the end of the class
declaration then the child’s virtual methods will be shifted.
2. Adding of a new field (of any kind) to a class declaration can cause following
problems:
1.Adding of a new field to the end of a class causes class size to change
as in the case of adding of a virtual method to a class that didn’t have
any. The client program will allocate less memory than it is required for a
new class and this will cause referencing to the memory out of the class
scope.
2.Adding of a new field between existing ones is even worse. It will case
all addresses of fields after the new one to be shifted and a client
application will work with the incorrect addresses while working with the
fields that are situated after the new field. We also have the problem as in
the previous point here.
There are other reasons that can cause DLL backwards compatibility problems, so below
is the list of solutions that are usually offered to overcome most of them.
This is the summary of solutions I’ve found in articles on the web and I’ve got while
talking with different developers.
The following conventions of a DLL development are usually offered to support DLLs
backwards compatibility:
1. Coding conventions:
o Each class exported from a DLL or its base class should have at least
one virtual method to add a virtual methods table to a class. This will
allow adding of virtual methods to it later.
o If you are going to add a virtual method to a class, add it after all
other virtual methods’ declarations. This will allow not to spoil the
order of methods in the virtual methods table.
o If you are planning to extend a class with the new fields later, then add
there a pointer to the structure where you will add them. In this case
while adding a new field, you will add it not to the class but to the
structure and so the size of the class won’t change. Work with the
structure as with the storage of additional class fields and add
corresponding fields’ accessors to the class declaration if it is required.
In this case, the DLL must be linked to a client application implicitly.
o To solve the problems stated in the previous points, you may also
make the pure interface classes for all exported classes, but in this
case it will be impossible to inherit these classes in the client
application and it will be also impossible to make the hierarchy of
exported classes in the DLL.
o Distribute two versions of the DLL and LIB (Debug/Release) because if
you will distribute only Release version then users who are developing
clients won’t be able to debug their application as Release and Debug
modes have different heap managers and thus if release DLL will
allocate memory which will be freed in the debug client application
(vice versa), it will cause a runtime failure. There is also another
possibility to solve this problem. You may add methods to allocate and
deallocate memory for the DLL classes to the DLL and also prohibit
deallocation of the client’s application objects in the DLL. But it is not
simple to maintain this solution.
o Don’t change the default parameters in the DLL classes’ methods as
they are moved to the client’s code at compile time.
o Beware of modification of inline methods.
o Check that all enumerations don’t have default element values. As
while adding/removing a new enumeration member you may shift
values of the old enumeration members. That is why each member
should have a unique value assigned to it. Also it should be
documented if enumeration can be extended, because in this case
client application developer should keep it in his mind.
o Don’t change macros in DLL headers.
2. Use DLL versioning: In the case of major DLL changes it is good to have an
ability to change the DLL name like Microsoft does with the MFC DLL. For
example, there can be the following DLL name template: Dll_name_xx.dll
Where xx is the number of the DLL version. Sometimes it is required to make
big changes in the DLL, which break its backwards compatibility. In this case,
the new name should be assigned to the DLL. While an installation to an OS,
a new DLL, the old one will not be removed and clients compiled with it will
still use it. At the same time, clients compiled with the new DLL will use the
new one.
3. DLL backwards compatibility testing: There are too many possibilities to break
the DLL’s backwards compatibility and that is why there should be some kind
of backwards compatibility testing.
Below I am going to discuss the problem of virtual methods and I will also give a
solution for one of its variants.
Let’s imagine that we have classes listed above. The first one is the my.dll member and
the second one is the class defined in the client’s application. Now we will make some
changes in the DLL class i.e. uncomment the following two lines:
and
//this->DoAnything();
Now if we won’t recompile the client’s application, then the VirtFunctClassChild class
will not know anything about the new virtual method void DoAnything() that was
added to its base class, so its table of virtual methods (vtable) will contain only the two
methods in the following order:
1. void DoSmth()
2. void DoSomething()
This is not correct because the vtable should be as follows:
1. void DoSmth()
2. void DoAnything()
3. void DoSomething()
Then if after instantiation of the VirtFunctClassChild class, someone will call its
void DoSmth() method, it will cause calling of the new virtual method void
DoAnything(). The base class knows that it should be a second method in the vtable
and so it will try to call it. Obviously this will cause calling of the void DoSomething()
method of the child class as the vtable of the VirtFunctClassChild class contains this
method in the second position and it doesn’t contain the void DoAnything() method at
all !!!!
It is important to note that in case of prohibiting of inheritance from the DLL classes with
adding of new virtual methods, we will still have the same problem because in this case
(just imagine that there was no virtual void DoSomething(){} method in the
VirtFunctClassChild class), the call of the void DoAnything() method from the
base class will cause referencing to the empty memory as there will be only one member
in the vtable.
Now it is obvious that there is a serious problem with adding of the new virtual methods
but there is an ability to solve it in case when virtual methods are used to handle
callbacks.
Now it is time to tell that these problems are not only the well know ones and that there
are not only special conventions that can help to solve them but that there are
technologies that allow to avoid most of the DLL backwards compatibility problems. One
of these technologies is Component Object Model (COM). So if you want to forget
problems with your DLLs, use COM or any other appropriate technology.
Let us now return to the task that was given to me. The task I was talking about in the
beginning of the article. It was to solve the backwards compatibility problem of one
product that was a DLL.
I knew about COM and thus my first proposal was to start using COM in this project to
overcome all the issues. This approach was rejected because of the following reasonable
causes:
1. The product already had a COM server on one of its inner layers.
2. It is rather expensive to rewrite a large set of interface classes into COM.
3. As this product is a DLL library, there are a lot of applications that use it, so
they did not want to force their customers to rewrite them.
In other words, what was required was to propose the most costless way to check the
DLL backwards compatibility problem.
Of course, I should say that the main issue of this project was adding the new fields and
virtual callback functions to the interface classes. The first problem is simply solved by
adding a pointer to a structure (that will contain all new fields) to the class declaration
and it is a common approach I have mentioned above. But the problem with the virtual
callback functions was new. Further I propose the costless and effective way to solve it.
Let us imagine that we have a DLL with the exported classes someone has to inherit
from, to implement virtual functions to handle callbacks in his application. We want to
make minor changes in the DLL. This change should allow us to painlessly add new
virtual call back functions to the DLL classes in the future. At the same time, we don’t
want to affect applications that use the current DLL version i.e. all we can expect is that
they will be once recompiled with the new DLL version but that should be done for the
last time. So these are target settings and here comes the solution:
We can leave every virtual callback method as it is in the DLL's classes. All we have to
remember is that adding of the new virtual method to any class definition may cause
failure if a client application would not be recompiled with the new DLL version. We
want to avoid this problem. The “Listeners” mechanism can help us here! If virtual
methods, defined and exported from the DLL classes, are used to handle callbacks, then
we can move new virtual methods to the separate interfaces.
#ifdef DLL_EXPORT
#define DLL_PREFIX __declspec(dllexport)
#else
#define DLL_PREFIX __declspec(dllimport)
#endif
#if defined(DLL_EXAMPLE_MODIFIED)
//Newly added interface extension
struct DLL_PREFIX ICallBack01 : public ICallBack
{
CLASS_UIID_DEF(ICallBack)
OBJECT_UIID_DEF
virtual void DoCallBack01(int event) = 0; //new call back method
};
#endif // defined(DLL_EXAMPLE_MODIFIED)
It is easy to see that all we have to do to prepare the DLL’s classes for extension with the
new virtual methods is to:
I have added the ICallBack01 interface definition here as an example of how the
CExample class can be extended with the new virtual callback method.
It is obvious that new virtual methods should be added to a new interface. One interface
for each DLL update (i.e. if we want to add several new virtual callback methods to one
class at the same time [for one DLL release], we should add them to one interface).
Each new interface (for one class) should extend the previous interface. In my example I
have only one extension interface ICallBack01. If in the next release we will need to add
a new call back virtual method to this class then we will create a new interface
ICallBack02 that will extend the ICallBack01 interface in the way the ICallBack01
extends the ICallBack.
Macros in the code above are used to define methods required to check the interface
version. For example when we are adding the new interface ICallBack01 with the new
method DoCallBack01, we should check (in the CExample class) if we can call it on the
ICallBack * mpHandler; member. This check should be done in the following way:
if(mpHandler != NULL && mpHandler-
>GetObjectUIID()>=ICallBack01::GetClassUIID()){
((ICallBack01 *) mpHandler)->DoCallBack01(2);
}
All these is made in the case when the new call back interface is added and the call back
is inserted in the CExample class, so it is very easy.
Now you can see that the client’s applications would not be affected by these changes.
The only thing will be required after the first DLL classes’ update (adding of macros,
base interface ICallBack definition, SetCallBackHandler method, ICallBack pointer)
is the client application's recompilation.
Later when someone will add the new callback, he will do it via the new interface
addition (like adding of the ICallBack01 interface in our example). It is obvious that
such change would not affect anything, as the order of virtual methods will not be
changed. So the client applications will work in the old manner. The only thing you
should remember is that client applications will not receive the new callbacks until they
implement the new interface.
It is also important to note that DLL users will still be able to work with it easily. This is
the client’s class example:
#if !defined(DLL_EXAMPLE_MODIFIED)
//Before a new interface is added to the dll.
class CClient : public CExample{
//Everything remains as it was
public:
CClient();
void DoCallBack(int event);
};
#else // !defined(DLL_EXAMPLE_MODIFIED)
//After the interface ICallBack01 was added client may (not
necessarily)
//change his class in the following manner to handle new events
class CClient : public CExample, public ICallBack01{
public:
//The constructor changes
CClient();
//Everything remains as it was
void DoCallBack(int event);
//Add the new virtual method definitions
//(for methods inherited from the ICallBack01)
void DoCallBack01(int event);
};
#endif // defined(DLL_EXAMPLE_MODIFIED)
Example Project
In the example workspace, you will find two sub projects:
To check that everything works, perform the following steps one by one:
That is all about the example project. Just download and explore it to get all details.
Conclusion
In this article:
I believe that this article will be helpful for anyone who has come across the ‘DLL Hell’.
Overview
Purpose
Because "COM Architecture" is so huge, I had to split it into two pieces. I defined the
unit of deployment (from a technical point of view we call it an Executable file or
DLL file) as the delimiter of the split. Therefore we have the two following groups:
You will learn how to make COM Server and Client Application. You will see how to
deploy and install them (on local machine and remote machine).
This series of articles want to be more practical than theoretical. However, you can
find many good books and articles that go deeper into details in the bibliography
section.
The series is called "COM Macro-Architecture Topology" and this is the main article.
It will give you:
However, beginners can still read this series and used the practical section like a
tutorial. From my own experience, I have started to learn COM by practice and then
books helped me to put it down and understand the theory.
I do remind to COM developers who are reading this series that the main subject is
COM architecture. I am not going to explain or compare the different COM helper
tools, C++ libraries (e.g. ATL) or even C++ design aspects (e.g. smart pointers or
wrapper classes) available to develop COM Client applications or Servers.
Requirements
All the code in these samples have been written in C++, using Visual C++ 6.0 (sp4)
under Windows 2000.
They also use the Active-X Template Library (ATL).
Downloading
Demo files - You will find the necessary tools (Executables, DLLs,
Registry configuration and Batch files) to try the different cases. Download
demo project - 127 KB
Source files - You will find all the source code for making the 3 different
COM Servers (Executable, 2DLLs and 1DLL) and the Client Application.
Download source - 70 KB
The project's structure on my computer is like that:
All the Executable and DLL files are output in the \Implementation\Bin directory.
Deployment considerations
Be careful when deploying the components described in this article.
Take into account these points:
• In order to install them you need to have the Administrator rights (Client and
Server machine).
• Allow the Server to use the identity of the Interactive user or a user's
account with the Interactive rights (so dialog boxes could be showed).
• When calling the Server remotely, be sure that the User associated to your
Server is locally logged-on onto the server machine as the Interactive user, so
that the Dialog boxes can appear.
• Using the "Terminal Services Client" does not give you the Interactive user
account, although your account has the Interactive rights (For further
information read [Bi6]).
The Interactive User is the user currently logged-on the Server machine.
Limitations
Although COM is available in many operating systems, I have just considered the
Windows implementation. This means that some aspects of COM could have different
restrictions based on their operating system implementation (e.g. the Security, the
COM information storage, etc.) .
Some Theory
Look at the article "COM IDs & Registry keys in a nutshell" for information on the
registry and COM vocabulary.
The activation mechanism is the process used by COM to locate, load and launch a
COM Server (DLL or Executable). The result, if all goes to plan, will be to
instantiate a COM object or COM class object. As you can understand,
Activation is a dynamic action and it occurs at runtime.
COM proposed 3 activation models to bring COM objects into memory (see [Bi1],
chapter 5 and [Bi11], chapter 3):
• Clients can ask COM to bind to the class object or class factory of a given
class (using CoGetClassObject()).
• Clients can ask COM to create new instances of a class based on a CLSID
(using CoCreateInstance/Ex()).
• Clients can ask COM to bring a persistent object to life based on the
persistent state of the object (using CoGetInstanceFromFile()).
You can also define a custom way to activate your COM object based on one of these
3 basic activation models.
All these activation models use the services of the COM Service Control Manager
(SCM). The COM SCM is not the same as the Windows NT or Windows 2000 SCM
(used to start logon-independent processes known as Windows Services). Every host
that supports COM has a COM SCM. His role is to activate the COM objects on a
particular machine (locally or remotely).
From a location point of view, COM Servers that reside on the client machine are
called "local servers" and the ones that reside on a remote machine are called
"remote servers".
This figure points out 3 results:
• the Executable COM Server must check the command line for these
switches:
o /RegServer or -RegServer: will insert keys in the registry and register
its class factories by calling CoRegisterClassObject().
o /UnregServer or -UnregServer: will suppress keys in the registry and
unregister its class factories by calling CoRevokeClassObject().
To register a DLL COM Server, you can use the utility REGSVR32.EXE in the
Win32 SDK. This application will call these 2 exported functions to register or
unregister the DLL COM server.
To illustrate the work done by COM in order to bind the Client application and the
COM Server together, let assume the client code is like this:
ISampleClass *psc = NULL;
HRESULT hr = CoGetClassObject(GUID1, CLSTX_ALL,
0, IID_IClassFactory,
(void**)&psc);
It is worth to know that environments such as Visual Basic and Visual Java
always use CLSTX_ALL, indicating that any available implementation will be
good ([Bi11], chapter 3).
Here is the Activation chain used by COM in order to locate, load and launch the COM
Server and return a interface pointer to the client (look at [Bi1], chapter 6, [Bi3],
chapter 5 and [Bi11], chapter 6):
In the figure above, a yellow rectangle indicates a registry key or subkey under
[HKEY_CLASSES_ROOT\CLSID] and a green rectangle indicates one under
[HKEY_CLASSES_ROOT\AppID].
For simplicity, I have not taken into account the security aspect. So, let see what
happens:
1. The client calls CoGetClassObject() which gives control to the COM Service
Control Manager (SCM). Because the third parameter of CoGetClassObject()
is not set with a host name, the SCM will first look in the local registry for the
key:
2. [HKCR\CLSID\{GUID1}]
o Notice that in Windows 2000, the SCM will look first under the key
[HKEY_CURRENT_USER] and then [HKEY_CLASSES_ROOT] (aliased as
[HKCR]).
o If the key does not exist locally, the SCM will then try to find
information on the Windows 2000 Active Directory and install the
component locally.
o If the key is not available, the call will fail with the HRESULT code
REGDB_E_CLASSNOTREG: CLSID is not properly registered.
This code error might also indicate that the value you specified in the
second parameter of CoGetClassObject() is not in the registry.
2. At this stage, the class ID GUID1 is available the SCM will then look for the
subkey:
3. [HKCR\CLSID\{GUID1}\InprocServer32]
@="C:\myserver.dll"
If the key is present, it should contain a valid path with a DLL file name. COM
will load the DLL into the client process space, call DllGetClassObject() to
get the IClassFactory interface pointer and return it to the client.
4. If this key is not present, then the SCM will look for the subkey:
5. [HKCR\CLSID\{GUID1}\InprocHandler32]
@="C:\myserver.dll"
Again, if the key is present, it should contain a valid path with a DLL file
name. COM will load the DLL into the client process space, call
DllGetClassObject() to get the IClassFactory interface pointer and return
it to the client.
If neither In-process subkey is available, the SCM will look for an Out-of-process
activation. After this stage, the client will always get back an interface pointer on a
proxy.
4. The SCM is now looking on its cache to see whether the requested COM class
object (class factory) is currently registered. If so, the SCM will return a
marshalled interface pointer to the client.
The COM server will register its class factories with the SCM using the helper
function CoRegisterClassObject().
If the server uses the flag REGCLS_SINGLEUSE, then the SCM will not put the
class factory as public level and so will not be available for subsequent
activation request. In fact, for subsequent activation calls, the SCM will start a
new process using the step #5 and followings.
5. If the class factory is not registered in its cache (or has been removed from
the public view of the SCM), this means that there is not any Out-of-process
currently running and providing this class factory as public level.
Under Windows NT 4.0 or 2000, COM supports 3 process models for creating
servers:
o NT Services.
o Normal processes.
o Surrogate processes. They are used to host DLL servers:
locally it provides fault isolation and security context.
remotely it provides remote activation and distributed
architecture.
Whatever the process model is used to create a server process, the server will
have 120 seconds (or 30 seconds under Windows NT service Pack 2 or earlier)
to register the requested class factory. If the server process fails to do so in
time, the SCM will fail the client's activation request ([Bi11], chapter 6).
When creating a server process, the SCM will first look for a local service. It
will try to get an AppID:
If it finds a value, e.g. GUID2, it will look for the named value under
the AppID root:
[HKCR\AppID\{GUID2}]
LocalService="MyService"
o If it can not find the AppID value GUID2 under the key
[HKCR\CLSID\{GUID1}] it takes the class ID value itself GUID1 as
an AppID and look under the AppID root:
o [HKCR\AppID\{GUID1}]
o LocalService="MyService"
In the figure above, GUID1 and GUID2 could have the same value, but it is
preferable if it is not the case.
GUID1 represents the class ID of the COM class factory whilst GUID2
represents the COM Application ID used by the COM class factory.
If a value exists, the (COM) SCM will ask to the Windows SCM to start the
service MyService. The service will register its class factories with the SCM
using the helper function CoRegisterClassObject(). Now, COM can query
for the class factory GUID1 and ask it to return a pointer for the
IClassFactory interface, which is to be marshalled back to the client.
6. If the SCM can not find this named value, then it will try to start a normal
process, so it will look for the subkey:
7. [HKCR\CLSID\{GUID1}\LocalServer32]
8. @="C:\myserver.exe"
If a value exists, the SCM will try to launch the local server by using API
functions such as CreateProcess() or CreateProcessAsUser().
After this step, the SCM could no start neither a Local service nor a Local Server,
so it would try to create an Out-of-process remotely.
7. If the SCM can not find a local server, it will look for the named value:
8. [HKCR\AppID\{GUID2}]
RemoteServerName="\\MyRemoteComputer"
If this value is available, the activation request will be forwarded to the SCM
on the designated host machine. It is worth noting that although the client
application will only use the CLSCTX_LOCAL_SERVER flag when querying the
activation, the request will be forwarded to the remote machine if no local
servers are registered.
The remote SCM will, following steps #4 through #6 (i.e. steps #7.c through
#7.e), try to find and launch a remote server that supports the requested
class factory. If it will succeed it will return a pointer for the IClassFactory
interface, which is to be marshalled back to the client machine.
If this named value exists, the remote SCM will start the Surrogate
process (e.g. MYSURROGATE.EXE) and ask it to load the DLL COM
server designated under the subkey:
[HKCR\CLSID\{GUID1}\InprocServer32]
@="C:\myserver.dll"
[HKCR\AppID\{GUID2}]
DllSurrogate=""
then the remote SCM will start the default Surrogate process (called
DLLHOST.EXE) and ask it to load the DLL COM server.
Maybe, you are now wondering "Why the SCM will not try to create a Surrogate
process after step #6 ?"
As I wrote before, there are three process models. Until step #6, the SCM have tried
to create the first two: NT service and normal process. The last process model is
Surrogate process and it can be used locally to load and activate DLL COM Server as
Out-of-process. Nevertheless to activate this kind of process locally the client
application must explicitly ask it to COM by using the CLSCTX_LOCAL_SERVER flag only
instead of the CLSCTX_ALL flag.
"Why ? (again)"
In order to use the Surrogate process the SCM still need to have the DLL file name
under the subkey [HKCR\CLSID\{GUID1}\InprocServer32]. That is fine, but
when the client specified CLSCTX_ALL, it is specified CLSCTX_INPROC_SERVER as
the same time, the SCM will look first for the subkey
[HKCR\CLSID\{GUID1}\InprocServer32] and if it exists then it will launch the
COM Server as an In-process server into the client space. In this situation, the SCM
will never reach the named value DllSurrogate.
Some remarks:
The Security
In general, you use a security infrastructure to protect your private data, or any
resource, against wrong hands.
At the beginning, COM did not have a specific security infrastructure to protect the
client or the server application. The release of Windows NT 4.0 introduced processes
and threads security levels and permitted server processes to be accessed remotely.
On the Windows platforms implementation (particularly NT 4.0 and 2000), the COM
security infrastructure is built by using:
In order to log on to Windows, the user must have a user account. Each user and
user group is identified by a unique value called a Security IDenfier (SID). Once the
user logged on successfully, Windows associated to the user an access token. This
access token contains information related to the user such as the user SID, the list of
the groups the user belong to and other privileges.
Every time a process is started under the session opened by the user, Windows (by
default) associated the user's access token to it. Then, when the process wants to
access a system object (e.g. a thread, a mutex, or a file) Windows compares the
security information attached to it and the access token of the process. After
comparison, Windows grants or denies the access.
Every system object can contain a Security Descriptor that is composed of:
Both types of access control lists (SACL and DACL) are lists of Access Control
Entries (ACEs). An ACE contains the SID of a user or group of user and some type of
access (e.g. Read, Write, Execute, etc.) that is either granted to (access-allowed
ACE) or denied (access-denied ACE).
This is an open architecture as third parties can develop new SSP and install them
without interfering with the existing ones. An application that already uses an SSP
would require few or maybe no modification with a new SSP.
• Launch security: Which security users are allowed to create a new object in
a new process ?
=> Protecting the server machine
• Access security: Which security users are allowed to call an object ?
=> Protecting the object
• Identity: What is the security user of the object itself ?
=> Controlling the object
• Connection policy: Integrity- can messages be altered? Privacy- can
messages be intercepted by others? Authentication- can the object find out or
even assume the identity of the caller?
=> Protecting the data
These aspects can be set at different levels, i.e. the security grain:
Nota: when talking about the last 3 levels (per-class, per-Interface, and per-method
call basis) they are generally called fined-grain security.
By Configuration you can not set all the security aspect information, and it is the
same with Programmation. For this reason, they are complementary.
Settings on the registry are also called the default settings.
If you set security information on more than one level, what happens ? In fact,
according to the security levels, you can override the settings of the lower level by a
higher level. Here is a figure pointing out the security level ordered:
And to terminate this high-level view of COM security model, some of these security
informations can be set only on the client side, only on the server side, or in both
sides.
All these parameters and combinations of settings make COM Security a huge
subject by itself, but if you are developing distributed applications you must look at
them.
Some Practice
After finishing a COM component, we have to deploy, install and configure it. There
could be many situations to deploy our components.
By deployment I mean:
• What kind of unit (Executable or DLL) will you use for your Client or your
Server ?
• Where do you copy the Client unit and the Server unit ?
The 12 cases
We will see case by case how to make and install our COM applications: Client &
Server.
Case 1
Case 2
Case 3
Case 4
Case 5
Case 6
Case 7
Case 8
Case 9
Case 10
Case 11
Case 12
Picture legends
Here are the picture symbol's legend used in the 12 cases:
Case 1
Here are the steps to build this case:
Legend
• Process relation:
o The client application and the COM server objects will never run in the
same process.
This depend on the initialisation of the Class Factories in the COM Server
(using the COM API function CoRegisterClassObject()) but this is out of the
scope of this series.
When you want to uninstall the system (Client application and COM Server):
1. Remove the Client application by simply deleting the Client executable file.
2. Unregister the COM Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)".
o Step 9 using the "Local consideration".
3. Remove the COM Server files by simply deleting the COM Server executable
file and the Proxy/Stub DLL file.
Case 2
• the COM DLL server loaded into the client process. COM refers as this server
as an in-process server.
• the COM DLL server running outside the client process but in the same
machine. COM refers as this server as a (local) out-of-process server.
In-process Server
Here are the steps to build this case:
• Process relation:
o The client application and the COM DLL server will run in the same
process.
• Other:
o The client code can access the COM object indirectly (links 1.a, 1.b and
1.c) if they are in different COM apartments or directly (link 2) if they
are in the same, but this matter is out of the scope of this series.
When you want to uninstall the system (Client application and COM DLL Server):
1. Remove the Client application by simply deleting the Client executable file.
2. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)".
o Step 9 using the "Local In-process consideration".
3. Remove the COM Server files by simply deleting the COM DLL Server file and
the Proxy/Stub DLL file.
Out-of-process Server
Here are the steps to build this case:
Legend
• Process relation:
o The client application and the COM DLL server will run in the same
process.
• Other:
o If in the client code you use flags such as
CLSCTX_INPROC_SERVER, CLSCTX_INPROC_HANDLER,
CLSCTX_SERVER or CLSCTX_ALL the in-process server will be
launched first by COM.
When you want to uninstall the system (Client application and COM DLL Server):
1. Remove the Client application by simply deleting the Client executable file.
2. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)".
o Step 9 using the "Local Surrogate consideration".
3. Remove the COM Server files by simply deleting the COM DLL Server file and
the Proxy/Stub DLL file.
Case 3
For this deployment, you can have 2 different configurations:
• the COM DLL server loaded into the client process. COM refers as this server
as an in-process server.
• the COM DLL server running outside the client process but in the same
machine. COM refers as this server as a (local) out-of-process server.
In-process Server
Here are the steps to build this case:
Legend
• Process relation:
o The client application and the COM DLL server will run in the same
process.
• Other:
o The client code can access the COM object indirectly (links 1.a, 1.b and
1.c) if they are in different COM apartments or directly (link 2) if they
are in the same, but this matter is out of the scope of this series.
When you want to uninstall the system (Client application and COM DLL Server):
1. Remove the Client application by simply deleting the Client executable file.
2. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)".
o Step 9 using the "Local In-process consideration".
3. Remove the COM DLL Server file by simply deleting the file.
Out-of-process Server
Here are the steps to build this case:
• Process relation:
o The client application and the COM DLL server will run in the same
process.
• Other:
o In that configuration, we have lost the benefit to have only one DLL.
Because we want our component to run out-of-process, the client
application still needs the marshalling code (Proxy/Stub code) in its
space process. Therefore, you have to build and install the Proxy/Stub
DLL.
o If in the client code you use flags such as
CLSCTX_INPROC_SERVER, CLSCTX_INPROC_HANDLER,
CLSCTX_SERVER or CLSCTX_ALL the in-process server will be
launched first by COM.
When you want to uninstall the system (Client application and COM DLL Server):
1. Remove the Client application by simply deleting the Client executable file.
2. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)".
o Step 9 using the "Local Surrogate consideration".
3. Remove the COM DLL Server file by simply deleting the file.
Here for deployment sake I have made the difference between Client DLL and
Executable, but there are "no differences" from a functional point a view. I mean the
Client code to write is the same in both cases.
However, with in-process COM Server (DLL) you have to take in account the COM
Apartment consideration, but this is outbound the subject of this article.
Obviously, if your client is also a COM server, you have to apply the same rules for
the COM server explained before. So:
Case 7
Here are the steps to build this case:
Legend
• Process relation:
o The client application and the COM server objects will never run on the
same machine and so in the same process.
This depend on the initialisation of the Class Factories in the COM Server
(using the COM API function CoRegisterClassObject()) but this is out of the
scope of this series.
• If you have more than one server machine (many HOST B), you can configure
client machines to use different server machine by simply changing the
"RemoteServerName" value (under the AppID key) in the registry.
You can as well use, in your client application code, the API function
CoCreateInstanceEx() to determine the machine on which the object will be
instantiated.
When you want to uninstall the system (Client application and COM Server):
1. Unregister the COM Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)". There are 2 unregistrations: one for
the Client machine and one for the Server machine.
o Step 9 using the "Remote consideration".
2. On the Client machine, remove the Client application and the Server
Proxy/Stub DLL by simply deleting these files.
3. On the Server machine, remove the COM Server files by simply deleting the
COM Server executable file and the Proxy/Stub DLL file.
Case 8
Here are the steps to build this case:
• Process relation:
o The client application and the COM server objects will never run on the
same machine and so in the same process.
• DCOM aspect
o You have to enable DCOM on both machines: Client and Server. You
can use DCOMCNFG.EXE (on the Default Properties tab).
• If you have more than one server machine (many HOST B), you can configure
client machines to use different server machine by simply changing the
"RemoteServerName" value (under the AppID key) in the registry.
You can as well use, in your client application code, the API function
CoCreateInstanceEx() to determine the machine on which the object will
be instantiated.
When you want to uninstall the system (Client application and COM DLL Server):
1. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)". There are 2 unregistrations: one for
the Client machine and one for the Server machine.
o Step 9 using the "Remote consideration".
2. On the Client machine, remove the Client application and the Server
Proxy/Stub DLL by simply deleting these files.
3. On the Server machine, remove the COM Server files by simply deleting the
COM DLL Server file and the Proxy/Stub DLL file.
Case 9
• Process relation:
o The client application and the COM server objects will never run on the
same machine and so in the same process.
• DCOM aspect
o You have to enable DCOM on both machines: Client and Server. You
can use DCOMCNFG.EXE (on the Default Properties tab).
• If you have more than one server machine (many HOST B), you can configure
client machines to use different server machine by simply changing the
"RemoteServerName" value (under the AppID key) in the registry.
You can as well use, in your client application code, the API function
CoCreateInstanceEx() to determine the machine on which the object will
be instantiated.
When you want to uninstall the system (Client application and COM DLL Server):
1. Unregister the COM DLL Server following the instructions in the article "COM
Macro-Architecture Topology (Servers)". There are 2 unregistrations: one for
the Client machine and one for the Server machine.
o Step 9 using the "Remote consideration".
2. On the Client machine, remove the Client application and the Server
Proxy/Stub DLL by simply deleting these files.
3. On the Server machine, remove the COM DLL Server file by simply deleting
the file.
Conclusion
When I started this series of articles I have tried to organise my work around a
duality idea and I chose the duality concept of Macro/Micro. Now, I maybe release
that a better duality concept would be Static/Dynamic:
One more remark about this article: I did not talk about mixing cases, such as local
and remote deployment at the same time (e.g. case 1 and 7). Maybe I will write this
section on a future update.
For my part, I will try to write the "COM Micro-Architecture" article, or maybe "COM
Dynamic-Architecture".
Thanks
I would like to thank Minh, my wife, to her support; Dave Haste and Anne-Sophie
Merot for spending the time to review my article.