Intro to COM Lab

Objective
This lab will give the you experience in writing and using COM components.  You will make a COM server in C++ that retrieves the current system time.  Then you will make a client of the component in Visual Basic that displays the time returned from the object.   The client will actually be a component itself.  It will be an ActiveX control that contains another ActiveX control and uses your component.  Don't worry, it sounds confusing, but it's not that bad.

 

Making the COM Server
1. Open Visual C++.
2. Go to File->New…->Projects. Create a Win32 Dynamic-Link Library.  Click OK.  On the dialog select A simple DLL project and click Finish.  Click OK on the next dialog.
3. Now you need to define your interface. Go to File->New...Files and create a Text File called clock.idl.
4. In order to include the definition for IUknown paste the following line at the top of your IDL file:

import "unknwn.idl";

5. Now paste in the following code to define the ISimpleClock interface:

[
    object,
    uuid(0C3E2E71-B93C-11d2-AAD0-006008449275)
]
interface ISimpleClock: IUnknown
{
    HRESULT Hours([out, retval] short *ps);
    HRESULT Mins([out, retval] short *ps);
    HRESULT Secs([out, retval] short *ps);
};


6. Now you need to define a library. MIDL uses the library statement to create a type library. Paste in the following code, then save and close the file.

[
    uuid(585E0B82-B927-11d2-AAD0-006008449275)
]
library ClockLib
{
    importlib("stdole32.tlb");

    [
        uuid(129494A1-BA2A-11d2-AAD0-006008449275)
    ]
    coclass SimpleClock
    {
        [default] interface ISimpleClock;
    }
}


7. Go into FileView. Right-click on clock.idl, and select Settings... Uncheck MkTypLib compatible, then click OK. Right-click clock.idl again and select Compile clock.idl. This will run MIDL, which compiles the IDL file to create the type library.
8. In order to reduce the number of files you need to redistribute, add the type library as a resource. Go to File->New...Files, and create a Text File called clock.rc. Add the following line, then save and close the file:

1 TYPELIB "debug\clock.tlb"

9. Now you need to implement the interface you just created. Go to File->New...Files, and create a C/C++ Header File called simpleclock.h. Paste the following code in, then save and close the file:

#ifndef SIMPLECLOCK_H
#define SIMPLECLOCK_H

#include "clock.h"

class CSimpleClock : public ISimpleClock
{
public:
CSimpleClock() : m_cRef(0) {}

// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);

// ISimpleClock methods
STDMETHODIMP Hours(short *ps);
STDMETHODIMP Mins(short *ps);
STDMETHODIMP Secs(short *ps);

protected:
virtual ~CSimpleClock() {}

private:
long m_cRef;

};

#endif


10. Go to File->New...Files, and create a C++ Source File called simpleclock.cpp. This is the actual implementation of the interface. You'll find all necessary explanations as comments in the code. Paste the following code in, then save and close the file:

#include <stdafx.h>
#include "simpleclock.h"
#include <initguid.h>
#include "clock_i.c"

// IUnknown methods

STDMETHODIMP CSimpleClock::QueryInterface(REFIID riid, void **ppv)
{
    // Test riid against all supported interfaces and
    // set *ppv to the subset of the object that corresponds
    // to the requested interface type using static_cast
    if (riid == IID_IUnknown)
        *ppv = static_cast<ISimpleClock *>(this);
    else if (riid == IID_ISimpleClock)
        *ppv = static_cast<ISimpleClock *>(this);
    else
        // If the requested interface is not supported, set *ppv to
        // null and return E_NOINTERFACE
        return (*ppv = 0), E_NOINTERFACE;
    // If the requested interface is supported, be sure to
    // call AddRef through the resultant pointer (*ppv). Use
    // reinterpret_cast to coerce the type of *ppv to IUnknown *
    reinterpret_cast<IUnknown*>(*ppv)->AddRef();
    return S_OK;
}

// the module locking routines are prototyped here
// but are defined in the clock.cpp file
extern void LockModule(void);
extern void UnlockModule(void);

STDMETHODIMP_(ULONG) CSimpleClock::AddRef(void)
{
    // Call LockModule if AddRef is being called the first time
    // for this object
    if (m_cRef == 0)
        LockModule();
    // Use InterlockedIncrement to increment the reference count
    // and return the updated result
    return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) CSimpleClock::Release(void)
{
    // Use InterlockedDecrement to decrement the reference count
    // and cache the new count in a temporary variable
    LONG res = InterlockedDecrement(&m_cRef);
    if (res == 0)
    {
        // if the new reference count is zero, call delete this to
        // destroy the current object.
        delete this;
        // Call UnlockModule when Release is called the last time
        // for this object
        UnlockModule();
    }
    // Return the new reference count
    return res;
}


// ISimpleClock methods

STDMETHODIMP CSimpleClock::Hours(short *ps)
{
    HRESULT hr;
    if (ps)
    {
        SYSTEMTIME st;
        GetLocalTime(&st);
        *ps=st.wHour;
        hr = S_OK;
    }
    else
        hr = E_POINTER;

    return hr;
}

STDMETHODIMP CSimpleClock::Mins(short *ps)
{
    HRESULT hr;
    if (ps)
    {
        SYSTEMTIME st;
        GetLocalTime(&st);
        *ps=st.wMinute;
        hr = S_OK;
    }
    else
        hr = E_POINTER;

    return hr;
}

STDMETHODIMP CSimpleClock::Secs(short *ps)
{
    HRESULT hr;
    if (ps)
    {
        SYSTEMTIME st;
        GetLocalTime(&st);
        *ps=st.wSecond;
        hr = S_OK;
    }
    else
        hr = E_POINTER;

    return hr;
}


11. Now you need need to create a class factory, which is another COM class responsible for creating instances of your CSimpleClock objects. Go to File->New...Files, and create a C/C++ Header File called factory.h. Paste the following code in, then save and close the file:

#ifndef FACTORY_H
#define FACTORY_H

///////////////////////////////////////////////////////////
//
// Class factory
//
class CFactory : public IClassFactory
{
public:
// IUnknown methods
STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
STDMETHODIMP_(ULONG) AddRef(void);
STDMETHODIMP_(ULONG) Release(void);
// IClassFactory methods
STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv);
STDMETHODIMP LockServer(BOOL bLock);
};

#endif

12. Go to File->New...Files, and create a C++ Source File called factory.cpp. You'll find all necessary explanations as comments in the code. Paste the following code in, then save and close the file:

#include <stdafx.h>
#include "factory.h"
#include "simpleclock.h"

STDMETHODIMP CFactory::QueryInterface(REFIID riid, void **ppv)
{
    // Test riid against all supported interfaces and
    // set *ppv to the subset of the object that corresponds
    // to the requested interface type using static_cast
    if (riid == IID_IClassFactory)
        *ppv = static_cast<IClassFactory*>(this);
    else if (riid == IID_IUnknown)
        *ppv = static_cast<IClassFactory*>(this);
    else
        // If the requested interface is not supported, set *ppv to
        // null and return E_NOINTERFACE
        return (*ppv = 0), E_NOINTERFACE;
    // If the requested interface is supported, be sure to
    // call AddRef through the resultant pointer (*ppv). Use
    // reinterpret_cast to coerce the type of *ppv to IUnknown *
    reinterpret_cast<IUnknown*>(*ppv)->AddRef();
    return S_OK;
}

// the module locking routines are prototyped here
// but are defined in the gadgetsrv.cpp file
extern void LockModule(void);
extern void UnlockModule(void);

STDMETHODIMP_(ULONG) CFactory::AddRef(void)
{
    // When building an inprocess server, call LockModule each
    // time AddRef is called. When building an outofprocess server,
    // never call LockModule. In either case, return any random
    // non-zero integer that makes you feel good.
    LockModule();
    return 2;
}

STDMETHODIMP_(ULONG) CFactory::Release(void)
{
    // When building an inprocess server, call UnlockModule each
    // time Release is called. When building an outofprocess server,
    // never call UnlockModule. In either case, return any random
    // non-zero integer that makes you feel good.
    UnlockModule();
    return 1;
}

STDMETHODIMP CFactory::CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv)
{
    // Assume failure and store a null pointer in *ppv.
    *ppv = 0;
    // If the caller is trying to aggregate, return CLASS_E_NOAGGREGATION.
    if (pUnkOuter)
        return CLASS_E_NOAGGREGATION;
    // Create a new instance of the Patient C++ class.
    CSimpleClock *pObj = new CSimpleClock;
    // If the new operation fails, return E_OUTOFMEMORY.
    if (!pObj)
        return E_OUTOFMEMORY;
    // Call AddRef on the new object to stabilize its reference count.
    pObj->AddRef();
    // Call QueryInterface on the new object to fetch an
    // interface pointer for the caller.
    HRESULT hr = pObj->QueryInterface(riid, ppv);
    // Call Release on the new object. If QueryInterface failed,
    // then this Release will destroy the object, ensuring no
    // resources are leaked.
    pObj->Release();
    // Return the HRESULT returned by the call to QueryInterface.
    return hr;
}

STDMETHODIMP CFactory::LockServer(BOOL bLock)
{
    // Call LockModule or UnlockModule, based on the state of the
    // first parameter. In either case, return S_OK.
    if (bLock)
        LockModule();
    else
        UnlockModule();
    return S_OK;
}


13. Next you need some functions to register and unregister the DLL. Since the code is particularly nasty I'll give you the necessary files. Save registry.h and registry.cpp in the same directory as your workspace. Go to Project->Add To Project->Files..., select the files you just downloaded, and hit OK.
14. Now you need to create the standard entry points that COM uses to hook into your component. Open the .cpp file containing the DllMain function. Replace the code in there with the following code in, then save and close the file. You'll find all necessary explanations as comments in the code.

#include "stdafx.h"
#include "clock.h"
#include "factory.h"
#include "registry.h"

///////////////////////////////////////////////////////////
//
// Global variables
//

// DLL module handle
static HMODULE g_hModule = NULL ;
// Friendly name of component
const char g_szFriendlyName[] = "This is a simple clock." ;
// Version-independent ProgID
const char g_szVerIndProgID[] = "Clock.SimpleClock" ;
// ProgID
const char g_szProgID[] = "Clock.SimpleClock.1" ;
// If building a DLL, the server must
// maintain its own lock count.
LONG g_cLocks = 0;
// the class factory
CFactory g_coClockClassObject;

// This routine will be called when interface pointers
// are duplicated and when LockServer(TRUE) calls arrive.
void LockModule(void)
{
InterlockedIncrement(&g_cLocks);
}

// This routine will be called when interface pointers
// are destroyed and when LockServer(FALSE) calls arrive.
void UnlockModule(void)
{
InterlockedDecrement(&g_cLocks);
}

///////////////////////////////////////////////////////////
//
// Exported functions
//

//
// Can DLL unload now?
//
STDAPI DllCanUnloadNow()
{
    // DllCanUnloadNow should return S_OK if the module lock
    // count is zero, S_FALSE otherwise.
    return g_cLocks ? S_FALSE : S_OK;
}

//
// Get class factory
//
STDAPI DllGetClassObject(const CLSID& rclsid,
                                            const IID& riid,
                                            void** ppv)
{
    // DllGetClassObject should look for the SimpleClock's CLSID
    // and use QueryInterface to fetch an interface pointer
    // to the SimpleClock class object (g_coClockClassObject). If any
    // other class is requested, the implementation should
    // set the resultant pointer to null and return
    // CLASS_E_CLASSNOTAVAILABLE.
    if (rclsid == CLSID_SimpleClock)
        return g_coClockClassObject.QueryInterface(riid, ppv);
    else
        return (*ppv = 0), CLASS_E_CLASSNOTAVAILABLE;
}

//
// Server registration
//
STDAPI DllRegisterServer()
{
    return RegisterServer(g_hModule,
                                     CLSID_SimpleClock,
                                     g_szFriendlyName,
                                     g_szVerIndProgID,
                                     g_szProgID);
}


//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
    return UnregisterServer(LIBID_ClockLib,
                                        CLSID_SimpleClock,
                                        g_szVerIndProgID,
                                        g_szProgID);
}

///////////////////////////////////////////////////////////
//
// DLL module information
//
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD dwReason,
void* lpReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH)
        g_hModule = (HMODULE) hModule;

    return TRUE;
}


15. Next you need to export the entry points using a DEF file. Go to File->New...->Files, and create a new Text File called clock.def. Paste the following code in, then save and close the file:

LIBRARY        clock
EXPORTS
        DllGetClassObject       PRIVATE
        DllCanUnloadNow     PRIVATE
        DllRegisterServer         PRIVATE
        DllUnregisterServer      PRIVATE


16. Go into FileView. Open StdAfx.h and paste in the following line of code below the line that says #include <windows.h> in order to include basic COM support, then save and close the file.

#include <ole2.h>

17. Finally, we need to change a few settings. Go to Project->Settings... Click on the Link tab. Change Output file name to Debug/clock.dll. You want the DLL to be automatically registered each time it's recompiled, so click on the Custom Build tab. Add the following under each edit box:

Description
    Performing registration

Commands
   regsvr32 /s /c "$(TargetPath)"
    echo regsvr32 exec. time > "$(OutDir)\regsvr32.trg


Outputs
    $(OutDir)\regsvr32.trg

18. Build the DLL. You should end up with a message that says "RegSvr32: DllRegisterServer in .\Debug\clock.dll succeeded."

 

Making the Client
1. Open Visual Basic.
2. On the New Project dialog box select ActiveX Control, then click Open.
3. Go to Project->Components... On the Controls tab scroll down and check Microsoft Access Calendar Control 7.0. Click OK.
4. A new button should appear on your toolbar. Click on it, then draw a calendar control on the form. You have just added an ActiveX control to your form.
5. Create three labels below the calendar called Label1, Label2, and Label3.
6. Create a button called Time.
7. Go to Project->References... Scroll down and check ClockLib. Click OK.  This hooks VB into your DLL.
8. Go to View->Code.
9. Paste in the following code, which declares your object:

Dim clk As ClockLib.SimpleClock

11. Double-click on the Time button. Paste the following code inside the function VB just created, which calls the time functions from the component you wrote in C++:

Label1.Caption = clk.Hours
Label2.Caption = clk.Mins
Label3.Caption = clk.Secs


10. Double-click on the form. Paste the following code inside the function VB just created, which instantiates the object, then calls the calls the Time_Click function:

Set clk = New ClockLib.SimpleClock
Time_Click


11. Now click the play button to test the control. Click the OK button on the dialog the pops up. You should end up with something that looks a little like this:

screenshot.gif (12947 bytes)


Solution is available here.