Variation I: A Raw Multilingual IDispatch

The first technique for implementing IDispatch is to implement the entire interface yourself. You process all the arguments to Invoke for each method and property, manually implement GetIDsOfNames, and implement degenerate GetTypeInfoCount and GetTypeInfo functions. This IDispatch technique involves no type information and requires that the controller call GetIDsOfNames to convert names to dispIDs. Of course, this requires the controller to assume that the name exists until the controller tries to invoke the name. If GetIDsOfNames fails, the invocation fails. In addition, the controller cannot perform any type or argument checking prior to making a call to Invoke and has to pass whatever it has from the script that it's executing. So the controller depends on Invoke to return errors if there is a problem. This is, in fact, exactly how Visual Basic 3 and DispTest work: they don't make direct use of an object's type information, and they depend on the object's IDispatch to provide dispIDs as well as type checking.

The Beeper1 sample (CHAP14\BEEPER1) implements IDispatch in this way and contains no type information. The interface is implemented in a separate class, CImpIDispatch, which is defined in BEEPER.H and implemented in BEEPER.CPP. This class maintains a backpointer to the CBeeper object and delegates all IUnknown calls to CBeeper's IUnknown members. CBeeper itself instantiates CImpIDispatch in the Init function, storing the pointer in a field m_pImpIDispatch; this pointer is deleted in CBeeper's destructor.

QueryInterface Behavior

Take a look at CBeeper::QueryInterface:


STDMETHODIMP CBeeper::QueryInterface(REFIID riid, PPVOID ppv)
{
*ppv=NULL;

if (IID_IUnknown==riid)
*ppv=this;

if (IID_IDispatch==riid œœ DIID_DIBeeper==riid)
*ppv=m_pImpIDispatch;

if (NULL!=*ppv)
{
((LPUNKNOWN)*ppv)->AddRef();
return NOERROR;
}

return ResultFromScode(E_NOINTERFACE);
}

For the most part, this is a typical QueryInterface implementation except that it recognizes two IIDs as our IDispatch implementation: IID_IDispatch and DIID_DIBeeper. The latter IID is the one that the Beeper object assigns to its dispinterface, hence the extra Ds in the name. You can find this IID defined in INC\BOOKGUID.H as {00021127-0000-0000-C000-000000000046}. Because an object can implement multiple dispinterfaces, each interface must be available through QueryInterface given the specific DIID of that dispinterface. Only one, however, is considered the default dispinterface, and only that one is available through a query for IID_IDispatch. Because this Beeper object has only one dispinterface, both IIDs can be used to query for that dispinterface. If Beeper implemented a second interface—something such as DIID_DITweeter—we would add only the following line of code to QueryInterface:


    if (DIID_DITweeter==riid)
*ppv=m_pImpIDispTweeter;

Here m_pImpIDispTweeter would be a separate IDispatch implementation from m_pImpIDispatch.

IDispatch::GetTypeInfo and IDispatch::GetTypeInfoCount

When an automation object doesn't support type information, implementing GetTypeInfo and GetTypeInfoCount is a snap:


STDMETHODIMP CImpIDispatch::GetTypeInfoCount(UINT *pctInfo)
{
*pctInfo=0;
return NOERROR;
}

STDMETHODIMP CImpIDispatch::GetTypeInfo(UINT itinfo, LCID lcid
, ITypeInfo **pptInfo)
{
*pptInfo=NULL;
return ResultFromScode(E_NOTIMPL);
}

Because there's no type information, GetTypeInfoCount honestly returns a 0 in the pctInfo out-parameters. Because the count is 0, a controller cannot expect GetTypeInfo to succeed, and, in fact, our implementation fails here (although according to the out-parameter rules, we have to set the return pointer to NULL anyway).

IDispatch::GetIDsOfNames

As we learned earlier, GetIDsOfNames is used to convert the names of properties and methods, as well as arguments to those methods, to the dispIDs that refer to those elements. GetIDsOfNames has the following signature:


STDMETHOD IDispatch::GetIDsOfNames(REFIID riid, OLECHAR **rgszNames
, UINT cNames, LCID lcid, DISPID *rgDispID)

Here riid must always be IID_NULL, rgszNames is an array of pointers to the strings to convert to dispIDs, cNames indicates the size of the rgszNames array, lcid is the language to use in translation, and rgDispID is an array of dispIDs (of size cNames as well) in which this function returns the converted dispIDs.

When cNames is 1, this function needs only to convert a method or a property name to a dispID. When cNames is greater than 1, the additional names are for arguments to a method. All the Beeper objects in this chapter bother only with the first name because the Beep method doesn't have any arguments. In any case, if any name in rgszNames cannot be converted, we store DISPID_UNKNOWN in the same position in rgDispID and return DISP_E_UNKNOWNNAME from this function. This way the controller knows which names were unrecognized.

Beeper1's implementation of GetIDsOfNames supports both English and German names. More precisely, Beeper1 supports primary German, primary English, and a neutral language, which is the same as English. If any other locale is given in lcid, we return DISP_E_UNKNOWNLCID, as required. Otherwise, we iterate through the known method and property names in the appropriate language and look for a match (case insensitive). If one is found, the dispID is stuffed into rgDispID[0], and we return with NOERROR. Otherwise, we return with DISP_E_UNKNOWNNAME. Here's the actual code:


STDMETHODIMP CImpIDispatch::GetIDsOfNames(REFIID riid
, OLECHAR **rgszNames, UINT cNames, LCID lcid, DISPID *rgDispID)
{
HRESULT hr;
int i;
int idsMin;
LPTSTR psz;

if (IID_NULL!=riid)
return ResultFromScode(DISP_E_UNKNOWNINTERFACE);

//Set up idsMin to the right stringtable in our resources.
switch (PRIMARYLANGID(lcid))
{
case LANG_NEUTRAL:
case LANG_ENGLISH:
idsMin=IDS_0_NAMESMIN;
break;

case LANG_GERMAN:
idsMin=IDS_7_NAMESMIN;
break;

default:
return ResultFromScode(DISP_E_UNKNOWNLCID);
}

/*
* The index in this loop happens to correspond to the DISPIDs
* for each element and also matches the stringtable entry
* ordering, where i+idsMin is the string to compare. If we
* find a match, i is the DISPID to return.
*/
rgDispID[0]=DISPID_UNKNOWN;
hr=ResultFromScode(DISP_E_UNKNOWNNAME);

psz=m_pObj->m_pszScratch;

for (i=0; i < CNAMES; i++)
{
LoadString(g_hInst, idsMin+i, psz, 256);
if (0==lstrcmpi(psz, rgszNames[0]))
{
//Found a match; return the DISPID.
rgDispID[0]=i;
hr=NOERROR;
break;
}
}

return hr;
}

The buffer m_pszScratch is a 256-character array preallocated in CBeeper::Init. This improves the performance of GetIDsOfNames and reduces the number of error conditions that might occur. The strings for each language are contained in separate stringtables in the DLL's resources, DBEEPER.RC:


STRINGTABLE
BEGIN
§
IDS_0_SOUND, "Sound"
IDS_0_BEEP, "Beep"
END

STRINGTABLE
BEGIN
§
IDS_7_SOUND, "Ton"
IDS_7_BEEP, "Piep"
END

There are a few other strings in each stringtable for exceptions, as we'll see shortly. The symbols used for the strings also indicate the language so that we can keep them straight.

Now, after the controller translates names into dispIDs (0 for Sound and 1 for Beep, the same as in other languages), there are no more locale-specific concerns anywhere in this object, as we'll see by looking at Invoke.

IDispatch::Invoke

Invoke is the real workhorse of Automation. Because we already know the purpose of all the arguments to this function, let's start with Beeper1's implementation code:


STDMETHODIMP CImpIDispatch::Invoke(DISPID dispID, REFIID riid
, LCID lcid, unsigned short wFlags, DISPPARAMS *pDispParams
, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
HRESULT hr;

//riid is supposed to be IID_NULL always.
if (IID_NULL!=riid)
return ResultFromScode(DISP_E_UNKNOWNINTERFACE);

switch (dispID)
{
case PROPERTY_SOUND:
if (DISPATCH_PROPERTYGET & wFlags
œœ DISPATCH_METHOD & wFlags)
{
if (NULL==pVarResult)
return ResultFromScode(E_INVALIDARG);

VariantInit(pVarResult);
V_VT(pVarResult)=VT_I4;
V_I4(pVarResult)=m_pObj->m_lSound;
return NOERROR;
}
else
{
//DISPATCH_PROPERTYPUT
long lSound;
int c;
VARIANT vt;

if (1!=pDispParams->cArgs)
return ResultFromScode(DISP_E_BADPARAMCOUNT);

c=pDispParams->cNamedArgs;
if (1!=c œœ (1==c && DISPID_PROPERTYPUT
!=pDispParams->rgdispidNamedArgs[0]))
return ResultFromScode(DISP_E_PARAMNOTOPTIONAL);

VariantInit(&vt);
hr=VariantChangeType(&vt, &pDispParams->rgvarg[0]
, 0, VT_I4);

if (FAILED(hr))
{
if (NULL!=puArgErr)
*puArgErr=0;

return hr;
}

lSound=vt.lVal;

if (MB_OK!=lSound && MB_ICONEXCLAMATION!=lSound
&& MB_ICONQUESTION!=lSound && MB_ICONHAND!=lSound
&& MB_ICONASTERISK!=lSound)
{
if (NULL==pExcepInfo)
return ResultFromScode(E_INVALIDARG);

pExcepInfo->wCode=EXCEPTION_INVALIDSOUND;
pExcepInfo->scode=
(SCODE)MAKELONG(EXCEPTION_INVALIDSOUND
, PRIMARYLANGID(lcid));

FillException(pExcepInfo);
return ResultFromScode(DISP_E_EXCEPTION);
}

//Everything checks out: save new value.
m_pObj->m_lSound=lSound;
}

break;

case METHOD_BEEP:
if (!(DISPATCH_METHOD & wFlags))
return ResultFromScode(DISP_E_MEMBERNOTFOUND);

if (0!=pDispParams->cArgs)
return ResultFromScode(DISP_E_BADPARAMCOUNT);

MessageBeep((UINT)m_pObj->m_lSound);

//The result of this method is the sound we played.
if (NULL!=pVarResult)
{
VariantInit(pVarResult);
V_VT(pVarResult)=VT_I4;
V_I4(pVarResult)=m_pObj->m_lSound;
}

break;

default:
ResultFromScode(DISP_E_MEMBERNOTFOUND);
}

return NOERROR;
}

One argument we didn't see earlier is riid. This must always be IID_NULL, or else you must return DISP_E_UNKNOWNINTERFACE. In other words, riid doesn't do anything for us. When OLE Automation was first being developed, the specifications indicated that the dispinterface ID was passed in this argument to support multiple dispinterfaces. But because you can query for secondary dispinterfaces using a DIID directly, this is not used. It wasn't removed from the specifications because Automation was already in beta at the time of the decision. It was left in and specified as a reserved value that must be IID_NULL.

The next few steps in our implementation are pretty intuitive: check which dispID is being invoked, check and coerce the types, and then attempt to get or put the property or execute the method, raising exceptions if necessary. You should not coerce types in pDispParams->rgvarg in place—use a local variable as shown here.

PROPERTY_SOUND, which is defined as dispID 0 (in BEEPER.H), can be invoked with the DISPATCH_PROPERTYGET, DISPATCH_METHOD, or DISPATCH_PROPERTYPUT flag. Because some controllers cannot differentiate a property get from a method call, we have to treat these as equivalent. In either case, we set the return VARIANT in pVarResult to contain the current value of m_lSound, and we're done. Easy!

To invoke METHOD_BEEP (defined as dispID 1 in BEEPER.H), we need first to ensure that the caller is actually trying to use this dispID as a method. Otherwise, we return DISP_E_MEMBERNOTFOUND. (This is, incidentally, the same error we return for an unrecognized dispID, as is the default case.) We also want to tell the controller that we don't take any parame-ters. If pDispParams->cArgs is nonzero, we fail with DISP_E_BADPARAMCOUNT; otherwise, we pass our m_lSound to MessageBeep to execute the method, store the sound played in pVarResult (just as a property get), and return successful.

With a property put operation, first protect any read-only properties you have, returning DISP_E_MEMBERNOTFOUND. I know this error isn't terribly informative, so it's likely that you'll see a DISP_E_ACCESSDENIED error added to OLE in the near future. You might also raise an exception as described on the following pages.

Sound is a writable property, so we first verify that we received one and only one argument in pDispParams, that the argument is named, and that the dispID of that argument is DISPID_PROPERTYPUT. If these conditions are not met, we return DISP_E_PARAMNOTOPTIONAL.

Next we attempt to coerce the argument we did get into a long because we require the use of VariantChangeType. The nice thing about using VariantChangeType is that if coercion doesn't work, this function returns the error code that we can return immediately to the controller to describe the error. In the case of an error, we have to specify which argument is in error in *puArgErr before returning. This example has only one argument, so the problem is always with the 0th position argument.

If we get this far, we have the right type of data, but we need to be sure that it is a value we can accept. This sample extracts the argument's value from the VARIANT through a direct dereferencing of pDispParams->rgvarg[0].lVal. This works fine for simple arguments. For more complex arguments to method calls, it is advisable to use the helper DispGetParam to address each argument by position in the method's argument list and combine the type coercion step. In other words, DispGetParam hides the grunge; a call to it would look like the following:


VARIANT    vtNew;

hr=DispGetParam(pDispParams, 0, VT_I4, &vtNew, puArgErr));

if (FAILED(hr))
return hr;

lSound=V_I4(vtnew); //Same as lSound=vtNew.lVal

In any case, we get the new value of the property and compare it against the possible MB_* values that allow for this property. If this checks out, we save the new value in m_lSound and return successful. If the validation fails, we have two choices: either fail Invoke trivially (with some useful error such as DISP_E_OVERFLOW or E_INVALIDARG) or raise an exception.

Raising an exception means filling the EXCEPINFO structure that the controller gave us and returning DISP_E_EXCEPTION. If the controller passes a NULL in pExcepInfo, you cannot raise exceptions and can only fail trivially. Fortunately, even DispTest asks for exception information, as do all versions of Visual Basic. You can choose to fill the exception structure at that time or fill in either the wCode or scode field and the pfnDeferredFillIn field for delayed filling. DispTest and Visual Basic 3 do not support deferred filling, but later versions do. You'll need a better controller than DispTest to try this feature. The code used in this sample to perform such a test is as follows:


INITEXCEPINFO(*pExcepInfo);
pExcepInfo->scode=(SCODE)MAKELONG(EXCEPTION_INVALIDSOUND
, PRIMARYLANGID(lcid));
pExcepInfo->pfnDeferredFillIn=FillException;

INITEXCEPINFO is a macro in INC\INOLE.H that clears an EXCEPINFO structure in one nice tidy line of code.

In both this code fragment and the previous listing of Invoke, I'm playing with fire by storing a 16-bit exception code and a 16-bit language ID in the scode field because there is no easy way to pass a locale to a deferred filling function. This is risky because I'm using scode in an unapproved way—it would be better if I had Invoke return different filling functions, depending on the language, or if I defined a set of error codes for each language so that the filling function knew what language to use. In any case, you can take a look at my shortcut lazy man's FillException function beginning on the following page.


HRESULT STDAPICALLTYPE FillException(EXCEPINFO *pExcepInfo)
{
SCODE scode;
LANGID langID;
USHORT wCode;
HRESULT hr;
LPTSTR psz;
LPOLESTR pszHelp;
UINT idsSource
UINT idsException;

if (NULL==pExcepInfo)
return ResultFromScode(E_INVALIDARG);

/*
* Parts of our implementation that raise exceptions; put the
* WORD exception code in the loword of scode and the LANGID
* in the hiword.
*/
scode=pExcepInfo->scode;
langID=HIWORD(scode);
wCode=LOWORD(scode);

//Allocate BSTRs for source and description strings.
psz=(LPTSTR)malloc(1024*sizeof(TCHAR));

if (NULL==psz)
return ResultFromScode(E_OUTOFMEMORY);

hr=NOERROR;

switch (wCode)
{
case EXCEPTION_INVALIDSOUND:
//Fill in unused information; macro in inole.h.
INITEXCEPINFO(*pExcepInfo);
pExcepInfo->wCode=wCode;
pExcepInfo->dwHelpContext=HID_SOUND_PROPERTY_LIMITATIONS;

//Set defaults.
pszHelp=OLETEXT("c:\\inole\\chap14\\beephelp\\beep0000.hlp");
idsSource=IDS_0_EXCEPTIONSOURCE;
idsException=IDS_0_EXCEPTIONINVALIDSOUND;

//Get the localized source and exception strings.
switch (langID)
{
case LANG_GERMAN:
idsSource=IDS_7_EXCEPTIONSOURCE;
idsException=IDS_7_EXCEPTIONINVALIDSOUND;
pszHelp=OLETEXT("c:\\inole\\chap14\\beephelp\\beep0007.hlp");
break;

case LANG_ENGLISH:
case LANG_NEUTRAL:
default:
break;
}

break;

default:
hr=ResultFromScode(E_FAIL);
}

if (SUCCEEDED(hr))
{
pExcepInfo->bstrHelpFile=SysAllocString(pszHelp);

LoadString(g_hInst, idsSource, psz, 1024);
pExcepInfo->bstrSource=SysAllocString(psz);
LoadString(g_hInst, idsException, psz, 1024);
pExcepInfo->bstrDescription=SysAllocString(psz);
}

free(psz);
return hr;
}

We store all the information necessary to describe the exception, including a help file, in the EXCEPINFO structure. The source and description strings for each language we support are in the stringtables in DBEEPER.RC:


//English
IDS_0_EXCEPTIONSOURCE: "Beeper.Object"
IDS_0_EXCEPTIONINVALIDSOUND: "The 'Sound' property can be set only\
to MB_OK (0), MB_ICONHAND (16), MB_ICONQUESTION (32)\
, MB_ICONEXCLAMATION (48), or MB_ICONASTERISK (64)."

//German
IDS_7_EXCEPTIONSOURCE: "Pieper.Objekt"
IDS_7_EXCEPTIONINVALIDSOUND: "Das 'Ton' Property kann nur die Werte\
MB_OK (0), MB_ICONHAND(16), MB_ICONQUESTION (32)\
, MB_ICONEXCLAMATION (48), oder MB_ICONASTERISK (64) erhalten."

Keep in mind that DispTest and Visual Basic 3 (but not later versions) ignore the help file and context ID that you provide through the exception. Controllers that pay attention to these fields will display a Help button in the same message box that displays the exception source and the description string that you also return from here. If the user clicks Help, the controller launches WinHelp with the help file and the context ID to display the correct help topic. If you run Beeper1 with the AutoCli sample from Chapter 15, you can see this working. The BEEPHELP directory in this chapter has the sources for the small help files (English and German) used with this example.

As a final note, an object should generally store only "<filename>.hlp" in bstrHelpFile, depending on the HELPDIR registry entry under the object's TypeLib entry to give controllers the installation path of the help files. Because Beeper1 doesn't have type information, there's no registry entry for the directory. Thus, we store the full path, and Chapter 15's AutoCli checks for a path before prepending the HELPDIR value.