This document is a tutorial on using COM in Borland's C++ Builder product. It is the first in a multi-part series that aims to show how Builder can "do COM" in a similar way to Microsoft's Visual C++ product. This may not be seen as a good thing by some people, but consider that you get the flexibility of VC++'s MIDL implementation using a much better compiler with access to VCL. I have used this combination in a variety of commercial projects and found that it is quite usable, especially as you no longer have to worry about Borland's code generator messing around with your source code all the time.
I also must say that I am not an expert in COM, I really just use it to create COM INPROC DLL servers, defining interfaces and type libraries as required. Technologies such as RPC and DCOM will not be covered as I know nothing about them.
Now to this tutorial, it will present the following items:
For this tutorial there will be no actual real code, I will be showing how to create a set of template files that can be used at the start of every project.
Why do you want to create a COM library? There can be many answers to this question, and the answers define how the COM library is created. It may be that you are just creating a plug-in interface for an existing application and don't want to get into doing dynamically loadable DLLs, or you may be creating a fully componentable application with each class (or a class of classes) defined as a loadable component rather than a hard coded unit in the application. In certain applications you might just want direct access to the interfaces in a library using C++, or you might want access to the interfaces using Windows Script Host or Visual Basic, this means that you have to decide whether or not your interfaces have to be dispatchable. Making your interfaces dispatchable has advantages and disadvantages. You always have access to your interfaces via the Windows scripting machanism (i.e. JScript, VBScript, PerlScript) but you can only use a subset of available types and cannot define your own. This means that the decision should be made early on whether or not to use dispatchable interfaces.
The process of creating a project is as simple as creating an appropriate directory structure. When using COM in this manner I have found that it is better to use Makefiles on the command line rather than trying to get BCB's IDE to do the MIDL compiling. Typically I set up the directories as such:
$(BaseDir)\$(ProjectDir)\idl - Stores .IDL files and .C & .TLB outputs generated by the compilation process
\lib - Stores .LIB files generated by the compilation process
\include - Stores project wide .H files
The idl directory is where the main files are placed for generating the interface definition files and the lib directory stores any library files created (which typically store GUIDs). The include directory will hold the interface definition header which can be used by both the implementing COM library and the using application to get the required information on the interfaces.
Both implementing COM libraries and applications can be stored within the $(BaseDir)\$(ProjectDir)\ directory structure and this is the way that I typically do it.
As projects using this method all use the command line, the best way to compile them is using a Makefile. A Makefile consists of a list of outputs, their inputs, and the commands required to turn the inputs to the outputs. An output of one item can be used as the input to another. The Make program also uses the date stamps on the files to determine whether the outputs are up to date or not.
For this system, there are multiple inputs and outputs that are either provided by the programmer or generated by the MIDL and BCC32 programs. The following table summarises these:
| File | Source | I/O | Description |
|---|---|---|---|
| .idl | Written by Programmer | I | Interface Definition Language files |
| .c | Written by Programmer | I | Contains GUID definitions for compiling into .lib files, that are useful for using throughout the project. |
| .c | Generated by MIDL | I/O | Contains GUID definitions of interfaces, coclasses and other relevant items. These along with the programmer GUID definitions are compiled into a single .lib file that can be linked into all the executables created by the project. Other .c files that can be generated by MIDL such as stub and proxy code are not used so are not generated. |
| .h | Generated by MIDL | O | Contains the C/C++ interface definitions that can be included by all the executables implementing or using the interfaces. |
| .obj | Generated by BCC32 | I/O | Object files for compiled C files. |
| .lib | Generated by TLIB | O | Library file containing the object files of all compiled C files. |
Now here is a Makefile that will handle all of the above inputs and outputs to generate the required outputs.
# The value projname should be set to the name of the idl file used
PROJECT= projname
# Defines the target that is built if no arguments are given to make
all: lib
# Defines the actual name of the library file we are creating. It goes into the lib sub-directory
lib: ..\lib\uuid.lib
# This creates the lib file. If more than one .idl file is used in the project, add their .obj names to both lists
# It is assuming there is a programmer created uuid.c file containing extra GUID definitions.
..\lib\uuid.lib: ${PROJECT}.obj uuid.obj
tlib ..\lib\uuid.lib +-${PROJECT}.obj +-uuid.obj
# This is a standard target definition to convert .idl files to .c files. In this file, if any of the files listed
# in the previous definition have a .idl file (i.e. ${PROJECT}.idl) then this step is used to convert the .idl files to .c files without
# an explicit step required. The generation of .tlb and .h files are not mentioned in this as they are not specific inputs to other
# steps.
.idl.c:
midl /cpp_cmd cpp32.exe /cpp_opt "-P- -oCON" /cstub nul /dlldata nul /h ..\include\$*.h /iid $*.c /proxy nul $*.idl
# This is a standard target definition to convert .c files to .obj files. In this file, if any of the files listed
# in the library creation step have a .c or .idl file, then this step will convert them to .obj files after make first
# does the .idl step first if required.
.c.obj:
bcc32 -c $*.c -o$*.obj
As you can see it is quite small, containing only about 6 important lines. It is worth noting that this Makefile does not take into account any of the dependancies that may be required for your project, so when building upon this Makefile, ensure that these are taken into account. Also note, that I am assuming some familiarity with the makefile syntax and am not pretending to instruct upon that.
The arguments for the midl command are important because they define how we can use midl with Borland's cpp32 preprocessor, and they ensure that extra files that we don't use are not created. If you have Visual C++ installed on your path, then you will not need the /cpp_cmd and /cpp_opt options as the cl preprocessor is used by default.
Microsoft's Platform SDK is the best source for information on the IDL syntax required for defining interfaces. All that this template will do is create a single vtable interface usable by a C++ program.
import "oleidl.idl";
[
uuid(XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX),
version(1.0)
]
interface IInterface : IUnknown
{
};
This file is saved in the idl subdirectory with the name set in the Makefile. At the command line in the idl subdirectory, you should be able to run the make command to generate the lib file and the include file for the interface. Ensure that you have created a uuid.c file or have deleted it's reference from the makefile. Also make sure that the $(BCB)\bin directory is on the path so you have access to all of the programs required. Have a look in the include directory to ensure that the projname.h file was created and contains a reference to your interface name.
In the template shown, I used the X character to fill out the UUID attribute. This value should be valid and can be generated using the guidgen.exe program found with either Visual C++ or the Platform SDK. If you have neither of these programs, then you can press CTRL-SHIFT-G key combination in the Builder IDE editor to generate a GUID which can be used as required.
We should now be ready to implement our interface and can now go back to the comfortable confines of Builder's IDE. As we are creating an INPROC DLL server, then we need to create a new DLL following these steps:
We now have a new file created with the entry point specified for our new DLL. Now hit the Save All button to save the project and main files. Put them into the $(BaseDir)\$(ProjectDir)\impl directory, naming them project.bpr and main.cpp.
The main.cpp file has to be written so that it contains all the necessary functions so that the DLL can be registered and unregistered, as well as queried for implementing CoClasses. A template to achieve this is shown below.
//---------------------------------------------------------------------------
#include "pch.h"
#pragma hdrstop
// Insert implementation unit headers here
#pragma link "uuid.lib"
//---------------------------------------------------------------------------
TComModule Module;
TComModule& _Module = Module;
//---------------------------------------------------------------------------
BEGIN_OBJECT_MAP(ObjectMap)
// Insert implementation CoClasses here - OBJECT_ENTRY(CLSID_CoClass,CoClass)
END_OBJECT_MAP()
//---------------------------------------------------------------------------
int WINAPI DLLEntryPoint(HINSTANCE hinst,unsigned long reason,void *lpReserved)
{
if(reason == DLL_PROCESS_ATTACH) {
_Module.Init(ObjectMap,hinst);
DisableThreadLibraryCalls(hinst);
}
return TRUE;
}
//---------------------------------------------------------------------------
void ModuleTerm(void)
{
_Module.Term();
}
#pragma exit ModuleTerm 63
//---------------------------------------------------------------------------
STDAPI __export DLLCanUnloadNow(void)
{
return (_Module.GetLockCount() == 0) ? S_OK : S_FALSE;
}
//---------------------------------------------------------------------------
STDAPI __export DLLGetClassObject(REFCLSID rclsid,REFIID riid,LPVOID *ppv)
{
return _Module.GetClassObject(rclsid,riid,ppv);
}
//---------------------------------------------------------------------------
STDAPI __export DLLRegisterServer(void)
{
return _Module.RegisterServer();
}
//---------------------------------------------------------------------------
STDAPI __export DLLUnregisterServer(void)
{
return _Module.UnregisterServer();
}
//---------------------------------------------------------------------------
This file can essentially remain the same for all projects that you might do, all that needs to be included is each of the implementation header files, and the object entries for each implementation that needs to be registered with the server.
There is an include called pch.h, this file is what I use for a standard precompiled header file for all projects. For a COM server implementation, there is some special includes that are required so below is the standard header file that should be expanded upon for your project includes.
//--------------------------------------------------------------------------- #ifndef __PROJECT_PCH_H__ #define __PROJECT_PCH_H__ #define NO_WIN32_LEAN_AND_MEAN // VCL main header include #include <vcl.h> // VCL other header includes // COM header includes #include <utilcls.h> #include <atlbase.h> #include <atlvcl.h> #include <atlcom.h> // Window header includes // RTL includes // STL includes //--------------------------------------------------------------------------- #endif
In order to get this to compile, you will have to set some options for the project. These are:
Other things that are not required, but good to have:
We are now ready to create our implementation of the interface. Again, I will show a template for both the header file and source file for a typical implementation, starting with the header file:
//---------------------------------------------------------------------------
#ifndef __MODULE_H__
#define __MODULE_H__
//---------------------------------------------------------------------------
extern const GUID CLSID_CoClass;
//---------------------------------------------------------------------------
class ATL_NO_VTABLE CoClass :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CoClass, &CLSID_CoClass>,
IInterface
{
public:
// Data used when registering Object
//
DECLARE_THREADING_MODEL(otApartment);
DECLARE_PROGID("ProgId");
DECLARE_DESCRIPTION("Description");
// Function invoked to (un)register object
//
static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
{
TComServerRegistrarT<CoClass> regObj(GetObjectCLSID(),GetProgID(),GetDescription());
return regObj.UpdateRegistry(bRegister);
}
BEGIN_COM_MAP(CoClass)
COM_INTERFACE_ENTRY(IInterface)
END_COM_MAP()
// Non-interface public members
public:
CoClass(void);
~CoClass(void);
// Private members
private:
// IInterface
public:
};
//---------------------------------------------------------------------------
#endif
//---------------------------------------------------------------------------
#include "pch.h"
#pragma hdrstop
#include "module.h"
//---------------------------------------------------------------------------
const GUID CLSID_CoClass = { 0xXXXXXXXX, 0xXXXX, 0xXXXX, { 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX } };
//---------------------------------------------------------------------------
CoClass::CoClass(void)
{
OutputDebugString(__FUNC__);
}
//---------------------------------------------------------------------------
CoClass::~CoClass(void)
{
OutputDebugString(__FUNC__);
}
//---------------------------------------------------------------------------
So all you need to do is create a new unit within the DLL project, and you can implement your interface. When all the code is written and the DLL has been successfully compiled, then it can be registered using the regsvr32 program that is found within the Windows system directory.
Well that is all that I want to include for this tutorial. This one has shown all the files required to create your own COM INPROC server, and their structure. Tutorial 2, will be a short one that will show the implementation of a single interface, and a simple Builder program that will use it. Tutorial 3 will include a simple dispatchable interface that can be used by the Windows Scripting Host system, or even a Visual Basic program.
Last updated: 16 October 2004
©Steven Haworth 2001-2004