Sean Baxter

COM Automation:
Type Information

1999

 

This document looks best at a high resolution on a big monitor.

This article is targeted at C++ programmers with some experience with COM.  For those new to COM, a good introductory text is Essential COM by Don Box.

Prologue  

What's the deal with Visual Basic?  VB is by no means a powerful or elegant language.  The people who use it certainly aren't mental giants.  So why are Visual Basic programmers often so darn productive?  The answer is simple my friends; you've suspected it all along.  They mooch off the hard work of C++ programmers like you and I. 

Visual Basic belongs to a host of applications called Automation controllers.   Other Automation controllers include Visual Basic for Applications, ASP, J Script, VB Script, and Visual J++ 6.0.  These are all very popular applications, you are thinking to yourself.  Are all these programmers dependent on the crafty skills of the C++ guru?  Of course they are!

As C++ programmers, isn't it in our best interest to stop this cycle of code reuse before it gets out of hand?  Actually no, and our reason is two fold.  First there is a humanitarian concern: if we stop providing functionality to VB programmers in the form of Automation servers, they'll lose their jobs and turn to drug addiction and chronic welfare.  The tax burden of rehabilitating and supporting these once-developers would be overwhelming.  The second reason to continue providing Visual Basic-compatible Automation servers is in the interest of greed: in some parts of the country VB programmers outnumber mosquitoes.  This is a huge marketplace, ready and willing to shell out big bucks for whatever cool objects they can incorporate in their projects.  C++ developers who recognize this emerging marketplace will have no trouble lining their pockets.  Imagine what you can buy with all of this cash: Italian sports cars, winter homes in Melbourne, and platinum toilet paper dispensers. 

The grand symbiosis between Automation controllers and Automation servers is made possible by COM (Component Object Model), a specification of the Open Active Group.  This article will explore, with mind boggling attention to every triviality, the COM mechanisms which facilitate Automation.  Readers will discover how Automation controllers like Visual Basic extract type information from COM components, and how to build type libraries into their very own components.

To kick things off, let's find out how Automation servers are used in Visual Basic programs.  After this we'll go through construction of the Profoundly Trivial Automation Server.  Keep your arms and legs inside the car at all times.

cmpndlg.gif (15409 bytes)

Figure 1

The infamous Components dialog box (Figure 1) lists the type libraries on the computer that provide ActiveX controls.  The innards of ActiveX controls will be meticulously studied in a future article; for now it is sufficient to say that ActiveX controls are COM components that can be inserted directly into a Visual Basic form. 

We've checked the Microsoft Windows Common Controls 6.0 type library in this dialog box, which adds buttons for each control in the library to the Visual Basic toolbox (Figure 2).

Visual Basic properties screenshot

Figure 2

The Microsoft Windows Common Controls 6.0 type library provides ActiveX versions of some of the common controls furnished to Windows API programmers through the InitCommonControls() function.  In a process that has infuriated C++ purists for ages, I've dragged a ProgressBar control from the Toolbox palette onto my form.  Docked to the right-hand side of the screen, the Properties Window conveniently lists all of the control's properties.   Changing the Orientation property to ccOrientationVertical makes the ProgressBar vertical.  I'm not the first to say, "This ain't programming!  Where's the obfuscation?"  Well, Visual Basic does less for the developer than you may suspect.  The burden of making development this easy sits squarely on the shoulders of the control programmer.  In Figure 2 we've selected the Appearance property.  There are two settings: cc3D (for the conventional 3D control) and ccFlat (for the super-trendy flat look that is so popular in applications like Encarta).   The question gnawing at your mind is, "How does VB know that ProgressBar supports both a flat and 3D look?  For that matter, how does VB know that the ProgressBar can be oriented vertically or horizontally?"  For this we consult the magical COM Type Info Browser, which will be constructed during the course of this article.

vbdemo2.gif (16793 bytes) vbdemo3.gif (17258 bytes)

Figure 3

 

Figure 4

 

vbdemo4.gif (16086 bytes)

Figure 5

In Figure 3, the Microsoft Windows Common Controls 6.0 type library is expanded (note that this is the same library as checked in Figure 1).   Sure enough, ProgressBar is a COM coclass (component object class) of this type library.  We notice that it supports two interfaces: IProgressBar and IProgressBarEvents.  Interesting.  In Figure 4 we expand the IProgressBar interface item.  Wow!  The properties from Figure 2 are all there.  We select the Appearance property and study its function signature at the bottom of the window.  Appearance takes an AppeanceConstants type.  Could this be, say, an enumeration?  In Figure 5 we expand the Enums item and see the AppearanceConstants type listed near the top.  So what are the values of the AppeanceConstants enum?  You guessed it: ccFlat and cc3D.

vbdem2_1.gif (12896 bytes)

Figure 6

ProgressBar is a component which can be dropped directly into a form, so naturally it works great with Visual Basic.  What about components that can't be dropped into forms?  As long as they support type information through Automation, they too function fine.  As an example, let's take Excel 97.  Trying to add excel.exe as a Component to the VB Toolbox gives an error, so you can't embed an Excel application directly into a form (although there does exist an ActiveX control for working with Excel workbooks).  We can, however, add Excel's type library through the VB Object Browser "References" dialog box, as shown in Figure 6.

vbdem2_2.gif (41391 bytes)

Figure 7

The VB Object Browser in Figure 7 crunches through the Excel type library and lists the library's coclasses in the left pane and their correponsing methods in the right pane.  In addition to the Object Browser (we'll write our own object browser later in this article), VB uses the information from this type library to provide context-sensitive help in the form of the Intellisense window.  As a C++ developer, packaging your components with a type library automatically makes them Object Browser and Intellisense-compatible in Visual Basic.  Additionally, they will work efficiently with all Automation controllers that support early binding (more on that later).

We'll now build an Automation server in our language of choice (C++).   Keep in mind how much work the type library does for you.

 

A Profoundly Trivial 
Automation Server 

Get the source: dll_hw.cpp  dll_hw.idl   dll_hw.rgs  dll_hw.def   dll_hw.dll

This very simple Automation server will expose a single dual interface, providing both late binding and early binding capabilities to the client.  The ATL Registrar will be used for registration of the DLL, and MIDL will genereate the type library and interface header from an IDL file.  The project is called dll_hw (as in DLL Hello World).  Let's start off by defining our interface, coclass, and library:

DLL_HW.IDL

[
    uuid(49FFDAE0-E465-11d2-A0FA-00E0291D8B19),
    helpstring("Sean's Test Automation Library"),
    version(1.0)
]
library SeanTestLibrary
{
    importlib("stdole2.tlb");
    [
        object,

        uuid(49FFDAE1-E465-11d2-A0FA-00E0291D8B19),
        helpstring("Sean Test Automation Interface"),
        oleautomation,
        dual
    ]
    interface ISeanTestInterface : IDispatch
    {
        HRESULT TestMethod();
    }
    [
        uuid(49FFDAE2-E465-11d2-A0FA-00E0291D8B19),
        helpstring("Sean Test Automation CoClass")
    ]
    coclass SeanTestCoClass
    {
        [default] interface ISeanTestInterface;
    }
}

The important thing to note is that the interface ISeanTestInterface is dual.  The oleautomation attribute guarantees that our methods only accept arguments that are VARIANT-compatible.  However since we have only a void method (and this is a dual interface), this is obviously no problem.  Also notice that our interface is defined inside the scope of the library block.  MIDL will generate no proxy/stub file, because the Universal Marshaller will work here (and besides, this is a DLL file).

Let's look at the ATL Registrar script:

DLL_HW.RGS

HKCR
{
    ForceRemove SeanTest.SeanTestCoClass = s 'Sean Test Automation CoClass'
    {
        CurVer = s 'SeanTest.SeanTestCoClass.1'
        CLSID = s '{49FFDAE2-E465-11d2-A0FA-00E0291D8B19}'
    }
    ForceRemove SeanTest.SeanTestCoClass.1 = s 'Sean Test Automation CoClass'
    {
        CLSID = s '{49FFDAE2-E465-11d2-A0FA-00E0291D8B19}'
    }
    NoRemove CLSID
    {
        ForceRemove {49FFDAE2-E465-11d2-A0FA-00E0291D8B19} = s 'Sean Test Automation CoClass'
        {
            ProgID = s 'SeanTest.SeanTestCoClass.1'
            VersionIndependentProgID = s 'SeanTest.SeanTestCoClass'
            InprocServer32 = s '%Module%'
            TypeLib = s '{49FFDAE0-E465-11d2-A0FA-00E0291D8B19}'
        }
    }
}

This is really boilerplate registration code.  The only thing perhaps new to you is the TypeLib key which identifies our type library by adding the GUID of the library block in DLL_HW.IDL.  We don't have to worry about registering the type library in HKEY_CLASSES_ROOT \ TypeLib here - that part will be handled by the COM runtime library.  Also, if you are new to the ATL Registrar, be aware that any identifier enclosed by percentage signs (in our example, '%Module%') will be replaced at runtime by some yet-to-be-determined value (in our example, the path name of the DLL).  One final remark: we did not specify a threading model.  Thus, dll_hw.h is a single-threaded server.  Production code would definately be apartment or multi-threaded, but specifying a single-threaded server makes this example easier; we don't need to worry about interlocked operations.

Finally, we get to some C++.  For this project to build, you first need to compile the IDL.  Run midl /h 'dll_hw.h' dll_hw.idl from the command line.  This generatess three files: dll_hw.h defines the interface ISeanTestInterface, dll_hw_i.c defines the GUIDs for library, interface, and coclass, and dll_hw.tlb is the binary type library.  Additionally, you need to create a new resource (say, resource.rc), and add both the registration file and binary type libraries as custom imports.  One requirement here: dll_hw.tlb needs resource ID 1.   "IDR_TYPELIB = 1" can be specified in the "Custom Resources Properies" dialog to accomplish this feat.  Why is this important?  LoadTypeLib will only find the type library if it has resource ID 1.  You can, of course, explicitly specify the resource ID of the type library by appending a comma followed by the resource ID to the filename, but that looks sort of silly.  Well, let's get to some code.

DLL_HW.CPP

#include <windows.h>
#include <atlbase.h>
#include <statreg.h>
#include "dll_hw.h"
#include "dll_hw_i.c"
#include "resource.h"

void RegisterServer(wchar_t* widePath, bool reg) {
    ATL::CRegObject ro;
    ro.AddReplacement(L"Module", widePath);
    reg ? ro.ResourceRegister(widePath, IDR_REGISTRY, L"REGISTRY") :
        ro.ResourceUnregister(widePath, IDR_REGISTRY, L"REGISTRY");
}

HINSTANCE hInstance;

int _stdcall DllMain(HINSTANCE hInstance, DWORD reason, void*) {
    ::hInstance = hInstance;
    return 1;
}

void GetPathName(wchar_t* widePath) {
    char ansiPath[MAX_PATH];
    GetModuleFileName(hInstance, ansiPath, MAX_PATH);
    MultiByteToWideChar(CP_ACP, 0, ansiPath, lstrlen(ansiPath) + 1, widePath, MAX_PATH);
}

extern "C" HRESULT _stdcall DllRegisterServer() {
    wchar_t widePath[MAX_PATH];
    GetPathName(widePath);
    RegisterServer(widePath, true);
    CComPtr<ITypeLib> pTypeLib;
    HRESULT hr(LoadTypeLib(widePath, &pTypeLib));
    if(hr) return SELFREG_E_TYPELIB;
    hr = RegisterTypeLib(pTypeLib, widePath, 0);
    if(hr) return SELFREG_E_TYPELIB;
    return S_OK;
}

extern "C" HRESULT _stdcall DllUnregisterServer() {
    wchar_t widePath[MAX_PATH];
    GetPathName(widePath);
    RegisterServer(widePath, false);
    HRESULT hr(UnRegisterTypeLib(LIBID_SeanTestLibrary, 1, 0, 0, SYS_WIN32));
    if(hr) return SELFREG_E_TYPELIB;
    return S_OK;
}

Again, it's boilerplate.  DllMain initializes the global variable hInstance with the HINSTANCE of the DLL (this is different from the HINSTANCE of the process).  RegisterServer creates an ATL CRegObject (the ATL Registrar), and requests that it replace all occurences of "Module" with the file path.   CRegObject::ResourceRegister runs the registration script in the file specified by the first argument, at the resource ID of the second argument, and at the resource type in the third argument.  DllRegisterServer registers the type library in HKEY_CLASSES_ROOT \ TypeLib with the RegisterTypeLib function, provided by the COM runtime library.  Likewise, DllUnregisterServer invokes UnRegisterTypeLib (also provided by the COM runtime) to unregister the type library.

Now that we have the (un)registration code, it's time to code the Automation server.  Our TestMethod (as declared in dll_hw.idl) will simply create a message box displaying a classic Simpsons quote.  Can you name the episode?

DLL_HW.CPP  (continued)

ULONG globalCount;

struct EVeryBadThing { };
class CSeanTestObject : public ISeanTestInterface {
    ULONG _refCount;
    CComPtr<ITypeInfo> _pTypeInfo;
    bool _dispatchCall;
public:
    CSeanTestObject() throw(EVeryBadThing) : _refCount(0), _dispatchCall(false) {
        CComPtr<ITypeLib> pTypeLib;
        HRESULT hr;
        hr = LoadRegTypeLib(LIBID_SeanTestLibrary, 1, 0, 0, &pTypeLib);
        if(hr) throw EVeryBadThing();
        CComPtr<ITypeInfo> pTypeInfo;
        hr = pTypeLib->GetTypeInfoOfGuid(IID_ISeanTestInterface, &_pTypeInfo);
        if(hr) throw EVeryBadThing();
    }
    virtual ULONG _stdcall AddRef() {
        ++globalCount;
        return ++_refCount;
    }
    virtual ULONG _stdcall Release() {
        --globalCount;
        ULONG ret(--_refCount);
        if(!ret) delete this;
        return ret;
    }
    virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
        if(!ppv) return E_POINTER;
        if(riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
        else if(riid == IID_IDispatch) *ppv = static_cast<IDispatch*>(this);
        else if(riid == IID_ISeanTestInterface) *ppv = static_cast<ISeanTestInterface*>(this);
        else return *ppv = 0, E_NOINTERFACE;
        return AddRef(), S_OK;
    }

    virtual HRESULT _stdcall GetIDsOfNames(REFIID riid, OLECHAR** rgszNames, UINT cNames,
        LCID lcid, DISPID* rgDispId) {
        if(lcid == 0 || lcid == 9 || lcid == 0x409)
            return _pTypeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId);
        return DISP_E_UNKNOWNLCID;
    }
    virtual HRESULT _stdcall GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) {
        if(iTInfo) return TYPE_E_ELEMENTNOTFOUND;
        if(!ppTInfo) return E_POINTER;
        if(lcid == 0 || lcid == 9 || lcid == 0x409)
            return _pTypeInfo.CopyTo(ppTInfo), S_OK;
        return *ppTInfo = 0, DISP_E_UNKNOWNLCID;
    }
    virtual HRESULT _stdcall GetTypeInfoCount(UINT* pctinfo) {
        if(!pctinfo) return E_POINTER;
        return *pctinfo = 1, S_OK;
    }
    virtual HRESULT _stdcall Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags,
        DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgError) {
        if(lcid == 0 || lcid == 9 || lcid == 0x409) {
            _dispatchCall = true;
            return _pTypeInfo->Invoke(this, dispIdMember, wFlags, pDispParams, pVarResult,
            pExcepInfo, puArgError);
        }
        return DISP_E_UNKNOWNLCID;
    } 
    virtual HRESULT _stdcall TestMethod() {
        MessageBox(0, "It was people!\nPeople soiled our green!\n\n\t- Ned Flanders",
            _dispatchCall ? "Dispatch Call" : "Virtual Call", MB_OK | MB_ICONINFORMATION);
        _dispatchCall = false;
        return S_OK;
    }
};

class CSeanTestObjectFactory : public IClassFactory {
    ULONG _refCount;
public:
    CSeanTestObjectFactory() : _refCount(0) { }
    virtual ULONG _stdcall AddRef() {
        ++globalCount;
        return ++_refCount;
    }
    virtual ULONG _stdcall Release() {
        --globalCount;
        ULONG ret(--_refCount);
        if(!ret) delete this;
        return ret;
    }
    virtual HRESULT _stdcall QueryInterface(REFIID riid, void** ppv) {
        if(!ppv) return E_POINTER;
        if(riid == IID_IUnknown) *ppv = static_cast<IUnknown*>(this);
        else if(riid == IID_IClassFactory) *ppv = static_cast<IClassFactory*>(this);
        else return *ppv = 0, E_NOINTERFACE;
        return AddRef(), S_OK;
    }
    virtual HRESULT _stdcall CreateInstance(IUnknown* pUnk, REFIID riid, void** ppv) {
        try {
            if(pUnk) return CLASS_E_NOAGGREGATION;
            CSeanTestObject* seanTestObject = return CSeanTestObject;
            HRESULT hr(seanTestObject->QueryInterface(riid, ppv));
            if(hr) delete seanTestObject;
            return hr;
        } catch(EVeryBadThing) { return E_UNEXPECTED; }
    }
    virtual HRESULT _stdcall LockServer(BOOL lock) { return S_OK; }
};
       
extern "C" HRESULT _stdcall DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) {
    if(rclsid != CLSID_SeanTestCoClass) return CLASS_E_CLASSNOTAVAILABLE;
    CSeanTestObjectFactory* testObjectFactory = new CSeanTestObjectFactory;
    HRESULT hr(testObjectFactory->QueryInterface(riid, ppv));
    if(hr) delete testObjectFactory;
    return hr;
}

extern "C" HRESULT _stdcall DllCanUnloadNow() {
    return globalCount ? S_FALSE : S_OK;
}

This large chunk of source surely leaves you with many questions.  The first being, "Why is it called 'DLL Hello World' if there is no 'Hello World'?".   Well, I originally put in a "Hello World" but I thought the Flanders quote was more appropriate, so I changed it.  Let's start at the top.  ULONG globalCount is the total reference count of the objects in the server.  Any AddRef increments this count, and any Release() decrements it.  DllCanUnloadNow is called when COM wants to free up memory by unloading the module.  We certainly do not want to let COM unload the server while there are outstanding references to objects, so DllCanUnloadNow returns S_FALSE when globalCount is greater than zero.

On the next line you see struct EVeryBadThing { };.  Whenever an error occurs in a class constructor, I huck one of these babies.   It's not a good idea to leave a C++ object in an undefined state.  Exceptions are the Standard mechanism for preventing an object's construction.  Class CSeanTestObject has three data members: _refCount, _pTypeInfo, and _dispatchCall.   _refCount is simply the reference count on the object; CSeanTestObjectFactory has a reference count by the same name.  Because ISeanTestInterface supports invocation through IDispatch::Invoke, we need a dispatch handler.  Type information saves the day!   ITypeInfo implements the methods of IDispatch, so we simply delegate to those.  We retrieve an ITypeLib interface pointer in the CSeanTestObject constructor by invoking the COM runtime function LoadRegTypeLib on the SeanTestLibrary GUID.  ITypeLib::GetTypeInfoOfGuid retrieves the ITypeInfo interface pointer for ISeanTestInterface.  If the SeanTestLibrary can't be loaded, an EVeryBadThing structure is thrown, and the class factory cleans up the mess and returns E_UNEXPECTED from its catch clause. 

My delegating methods GetIDsOfNames, GetTypeInfo, and Invoke require special attention.  Notice that each function takes a locale ID argument (LCID).  However, _pTypeInfo points to only one type library: the system defaut (English) library.  The GetIDsOfNames and Invoke methods of ITypeInfo don't even take an LCID argument; they use whatever LCID was specified in LoadRegTypeLib.  So what happens if the Automation client requests an implementation of a non-English method?  I return a DISP_E_UNKNOWNLCID error code.  Learn English, you bums!

"If English was good enough for Jesus Christ, it ought to be good enough for the children of Texas."

- Miriam "Ma" Ferguson, former governor of Texas.

 

"If English was good enough for Jesus Christ, it's good enough for me."

- Unattributed Representative speaking to Dr. David Edwards,
head of the Joint National Committee on Language.

 

"If English was good enough for Jesus Christ, it's good enough for my Automation clients."

- Sean Baxter, offical COM programmer of the 2000 Summer Olympics.

ISeanTestInterface::TestMethod creates a message box that indicates the interface used to invoke the method (either the dispatch interface or the virtual interface).  The _dispatchCall flag is set in the Invoke method and cleared after the message box is displayed in the TestMethod function.  We can use this functionality to test when our Automation client (Visual Basic) makes the efficient virtual call versus the sluggish dispatch call.

Before any client can connect to this server, we need to export its entry points.   Just add the following DEF file to the project and build:

DLL_HW.DEF

EXPORTS
    DllRegisterServer @1 PRIVATE
    DllUnregisterServer @2 PRIVATE
    DllGetClassObject @3 PRIVATE
    DllCanUnloadNow @4 PRIVATE

Yahoo!  After compilation has completed, run regsvr32 on this sucker to add the registry entries.  Take a look at the registry now:

reg1.gif (10874 bytes)

Figure 8

Figure 8 shows the Prog IDs we registered in HKEY_CLASSES_ROOT.  Each ProgID entry has a CLSID key that points to an entry in HKEY_CLASSES_ROOT \ CLSID.  Let's check that out.

reg2.gif (11257 bytes)

Figure 9

Figure 9 shows the coclass registry information.  InprocServer32 holds the path name of the DLL server; ProgID and VersionIndependentProgID point back to the entries in Figure 8.   The interesting key here is TypeLib, whose value is the GUID of the type library defined in dll_hw.idl

reg3.gif (11546 bytes)

Figure 10

Figure 10 finds our type library in the HKEY_CLASSES_ROOT \ TypeLib hive of the registry.  Recall that these entries were added by the RegisterTypeLib procedure of the COM runtime library.  The "1.0" key indicates the version of the type library.  The version MIDL attribute sets this value (check dll_hw.idl to verify this yourself).  Three keys sit under "1.0."  HELPDIR specifies the directory in which the type library's help file can be found (this directory complements the MIDL attributes helpcontext and helpfile).   The third argument of RegisterTypeLib becomes the HELPDIR value.  We passed zero, so the current directory is specified.  The FLAGS key controls which Automation clients can use the type library.  The value of FLAGS is any combination of values in the LIBFLAGS enumeration (check the SDK).  These flags are enabled by the control, restricted, and hidden MIDL attributes.  The third key, '0', indicates the locale ID (LCID) of the type library.  The LCID is specified with the MIDL attribute lcid.   dll_hw.idl did not specify an LCID, so the system default '0' is assigned for us.  The LCID subkey indicates the server's platform.  The value of this key specifies the path of the server.  With everything registered correctly in Figure 10, we're ready to test our server!

vbdem3_1.gif (11676 bytes)

Figure 11

Opening the Visual Basic Object Browser and launching the "References" dialog box reveals something like Figure 11Sean's Test Automation Library is listed, as hoped for.  But where did the string "Sean's Test Automation Library" come from?  Scroll back up to dll_hw.idl, and you'll recognize it as the library's helpstring attribute.  Is this blowing your mind yet?  The type library is manipulating the Automation client like Don King manipulates the Nevada boxing commission.

vbdem3_2.gif (30735 bytes)

Figure 12

Oh what joy!  The Object Browser properly lists the TestMethod function of SeanTestCoClass.  We create a new SeanTestCoClass object and invoke TestMethod.  Notice that the Intellisense helps us along.  What is the output of this program?

vbdem3_3.gif (8268 bytes)

Figure 13

As Figure 13 shows, Visual Basic made a virtual call to TestMethod.   Provided with Sean's Test Automation Library, VB found the offset of TestMethod in ISeanTestInterface and made the call directly.  Yippie.  Now let's try invoking TestMethod without giving VB any type information.

vbdem4_1.gif (21734 bytes)

Figure 14

Figure 14 Dims testObject as type Object.   In C++ terms, testObject is an IDispatch pointer.   The following call to CreateObject effectively does three things: CLSIDFromString is invoked to resolve the SeanTestCoClass CLSID, CoCreateInstance is called on the CLSID to create the object, and the IDispatch pointer is set with a call to QueryInterface.  Will the TestMethod function be called virtually, or through the dispatch interface?

vbdem4_2.gif (7277 bytes)

Figure 15

Aha!  A dispatch call!  To find the DISPID of TestMethod, Visual Basic calls our server's IDispatch::GetIDsOfNames implemention, which delegates to our type library. 

So what did we learn from these exploits?  Visual Basic will make the most efficient calls possible.  If type information is available, Visual Basic will coerce all parameters to the appropriate types itself.  If type information is not available, dispatch calls must be made.  While VB can resolve the vtable offsets of the methods at run time (I'll show you how in Part II of this article), it still requires IDispatch::Invoke's type coercion functionality.

 

Now that we've written an Automation server, let's dive into the steaming guts of COM type information.  We will write a type information browser!

Part II.

 



[COMMENTS]

Copyright © Sean BAXTER - http://ript.net/~spec/