Component Object Model API

Another approach that can be used to create an instance of a scripting component in your C++ program is to use the COM API directly. Generally speaking this option should only be used if absolutely necessary; it is a more complex process and involves more coding than either using CWnd derived components or the #import directive.

Creating Objects

The first step is to create the header file for the interface defined in the component's type library. This will require two tools that are included with Visual C++ and the Microsoft Platform SDK; the COM Object Viewer, and the Microsoft Interface Definition Language (MIDL) compiler. These tools can also be downloaded from Microsoft from their MSDN resources section of the website.

To create the interface definition (IDL) file, start the COM Object Viewer, select the Control folder and then the component you are interested in. For purposes of this example, we'll use the File Transfer Protocol component. Right click on the component and select View Type Information. This will open the ITypeLib Viewer window which contains the interface definition. Select File | Save As and save it as csftpctl.idl in the project directory. Close the viewer window and exit the COM Object Viewer.

Once the IDL file has been created, open the IDL file in the editor and look for a series of enum typedefs which define the constants for the component:

typedef [public]
    _ftpOptionsConstants ftpOptionsConstants;

    typedef enum {
        ftpOptionHttpNocache = 1,
        ftpOptionFtpSecureAuth = 8192
    } _ftpOptionsConstants;

These declarations are a side-effect of how the COM Object Viewer generates the IDL file and it needs to be cleaned up a bit so that the MIDL compiler generates the correct header file. Remove the typedef [public] section before each typedef enum section in the file. In other words, you would want to remove each section that looks like:

typedef [public]
    _ftpOptionsConstants ftpOptionsConstants;

The actual enum typedefs can stay in the IDL so that they're included in the header file and can be used by the application. Note that if these extraneous typedefs aren't removed, the MIDL compiler will generate duplicate enums in the header file and will cause compiler errors.

Save the IDL file and then use the MIDL compiler to generate the header file which will be included with your project. From the command line, enter:

midl /Oicf /W1 /Zp8 /h csftpctl.h /iid csftpctl_i.c csftpctl.idl

This will create three files: csftpctl.h, csftpctl_i.c and csftpctl.tlb. The TLB is the compiled type library and isn't needed for this example. The csftpctl.h header file contains the interface definition for the component, and the csftpctl.c file is a C source file which defines the GUIDs used by the component. Both of these files should be included in your project, typically in the source module where the component will be used. Note that the MIDL compiler may emit a warning that there are too many methods in the interface. This is a warning that applies only to Windows NT 4.0 and doesn't affect Windows 2000 or later versions of the operating system.

Now that the header file for the component interface has been created, the next step is to create an instance of the component. Define a member variable that is a pointer to the interface which looks like this:

IFtpObject *m_pIFtpObject;

Safe programming practices would also ensure that the pointer is initialized to NULL in the constructor to avoid potential errors when referencing the variable. As with the previous examples, the COM subsystem must be initialized. For MFC based applications, this can be done by calling AfxOleInit in the InitInstance function for the CWinApp derived application class. For other applications, CoInitializeEx should be called as:

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if (FAILED(hr))
{
    // Unable to initialize COM subsystem
    return;
}

Then the following code can be used to create an instance of the component:

HRESULT hr;
IClassFactory *pFactory = NULL;
IUnknown *pUnknown = NULL;

m_pIFtpObject = NULL;

hr = CoGetClassObject(CLSID_FtpObject, 
                      CLSCTX_INPROC_SERVER,
                      NULL,
                      IID_IClassFactory,
                      (LPVOID *)&pFactory);

if (FAILED(hr))
{
    // Unable to get the class factory interface for the
    // component, probably because it isn't registered
    return FALSE;
}

// Create an instance of the component
hr = pFactory->CreateInstance(NULL, IID_IUnknown, (LPVOID *)&pUnknown);
pFactory->Release();

if (FAILED(hr))
{
    // Unable to create an instance of the component
    return FALSE;
}

hr = pUnknown->QueryInterface(IID_IFtpObject,
                              (LPVOID *)&m_pIFtpObject);

if (FAILED(hr))
{
    // Unable to get the interface to the component
    return FALSE;
}

The CoGetClassObject function is used to get an interface pointer to the component's class factory, which actually does the work of creating an instance of the class. The CreateInstance member function creates an instance of the component and returns a pointer to its IUnknown interface. The interface to the class factory is released and then QueryInterface is called on the returned pointer to obtain the interface to the component's properties and methods.

Initialization

After an instance of the object has been created, the next step that you need to take is to call the Initialize method. This will prepare the component for use, validating the runtime license, loading any required networking libraries and allocating the system resources that it requires. The Initialize method has several optional arguments, however in most cases the only argument that is required is the runtime license key. This is a string of characters which is used to validate your development license. It is important to note that the runtime key is not your product serial number. For more information, refer to the section on Component Initialization. The following code demonstrates how the FTP component can be initialized:

_variant_t varLicKey;
_variant_t varError;
USES_CONVERSION;

// Create the runtime license key defined in csrtkey6.h
varLicKey = SysAllocString(T2OLE(CSTOOLS6_LICENSE_KEY));

// Initialize the component
varError = m_pIFtpObject->Initialize(varLicKey);
if (V_I4(&varError) != 0)
{
    AfxMessageBox(_T("Component initialization failed"), MB_ICONEXCLAMATION);
    return;
}

The runtime license key is created by converting it to Unicode and then calling SysAllocString to create a BSTR string. Because the component methods use variants, this key is assigned to a variant. Note that the _variant_t type is used, which is a COM support class which encapsulates a variant. The Initialize method returns a long integer variant which specifies an error code. A value of zero indicates that the component was successfully initialized, while a non-zero value is an error code.

The code to use the interface is similar to the previous example, however there are several significant differences:

COleVariant varServerName(m_strServerName);
COleVariant varServerPort(m_nServerPort);
COleVariant varUserName(m_strUserName);
COleVariant varPassword(m_strPassword);
COleVariant varAccount(m_strAccount);
COleVariant varTimeout(m_nTimeout);
COleVariant varLocalFile(m_strLocalFile);
COleVariant varRemoteFile(m_strRemoteFile);
COleVariant varOptions;
COleVariant varError;
HRESULT hr;

hr = m_pIFtpObject->Connect(varServerName,
                            varServerPort,
                            varUserName,
                            varPassword,
                            varAccount,
                            varTimeout,
                            varOptions,
                            &varError);

if (V_I4(&varError) != 0)
{
    CString strError;
    BSTR bstrError;
    USES_CONVERSION;

    hr = m_pIFtpObject->get_LastErrorString(&bstrError);
    if (FAILED(hr))
        return;

    strError.Format(_T("Unable to connect to %s\n%s"),
                    m_strServerName,
                    OLE2T(bstrError));

    AfxMessageBox(strError, MB_ICONEXCLAMATION, 0);
}
else
{
    CString strMessage;
    COleVariant varRestartOffset;
    LONG nBytes = 0;

    hr = m_pIFtpObject->GetFile(varLocalFile, 
                                varRemoteFile, 
                                varRestartOffset,
                                &varError);

    if (V_I4(&varError) != 0)
    {
        CString strError;
        BSTR bstrError;
         USES_CONVERSION;

        hr = m_pIFtpObject->get_LastErrorString(&bstrError);
        if (FAILED(hr))
                return;

        strError.Format(_T("Unable to download %s\n%s"),
                    m_strRemoteFile,
                    OLE2T(bstrError));

        AfxMessageBox(strError, MB_ICONEXCLAMATION, 0);
    }
    else
    {
        hr = m_pIFtpObject->get_TransferBytes(&nBytes);
        if (FAILED(hr))
            return;
        strMessage.Format(_T("Transferred %ld bytes of %s"), 
                           nBytes, m_strRemoteFile);
        AfxMessageBox(strMessage, MB_ICONINFORMATION, 0);
    }
    
    m_pIFtpObject->Disconnect(&varError);
}

As with the version of the code using the COM smart pointer, p_IFtpObject is a pointer to the interface, which requires that the -> operator be used to access its member functions. Property values are read using accessor functions that are prefixed with "get_", while those which set properties are prefixed with "put_". For example, to get the value of the TransferBytes property, the function name would be get_TransferBytes. Methods in the component are called using the same name.

Another difference is that all of the functions return HRESULT values, with the actual property value or return value from the method specified as a function parameter that is passed by reference. This is why the varError variable is passed as the last argument to the Connect method. If the HRESULT return value is non-zero, this typically will indicate an error. The error may be specific to the component, or it may be a general error coming from the COM subsystem.

Once the application is done using the component, the interface must be released with code like this:

if (m_pIFtpObject)
    m_pIFtpObject->Release();

Each component that is created has a reference count which is used to keep track of how many times one of its interfaces has been requested. When the reference count drops to zero, the component destroys itself and releases the memory that was allocated. Failing to release the interface will prevent the component from ever being destroyed and will result in a memory leak.


Copyright © 2008 Catalyst Development Corporation. All rights reserved.