Call the Dispinterface

While executing an automation script, a controller will get or put properties and call object methods. Which operation is performed depends on how an expression such as Object.Property or Object.Method is used in the controller's scripting language. A specific controller that drives a specific object (such as AutoCli) may not have a language script, in which case the controller's structure and code determine which operation is used. We'll look at each case in turn, but first we have to know which dispID to send to Invoke in order to perform the operation.

Mapping a Name to a dispID

Mapping the text names (which are used in scripting languages) of methods and properties to a dispID is the first step in late binding to the object's features. There are two ways to accomplish this. The first, demonstrated in AutoCli and also used in DispTest and Visual Basic 3, is to call IDispatch::GetIDsOfNames before calling Invoke. AutoCli has the function CApp::NameToID that performs the lookup:


    §
//Elsewhere
hr=pApp->NameToID(OLETEXT("Sound"), &dispID);
§

HRESULT CApp::NameToID(OLECHAR *pszName, DISPID *pDispID)
{
HRESULT hr;
TCHAR szMsg[80];

hr=m_pIDispatch->GetIDsOfNames(IID_NULL, &pszName, 1
, m_lcid, pDispID);

if (FAILED(hr))
{
wsprintf(szMsg
, TEXT("GetIDsOfNames on '%s' failed with 0x%lX")
, pszName, hr);
Message(szMsg);
}

return hr;
}

This is about the least efficient way, however, to accomplish the name-to-dispID mapping, especially when an out-of-process object is being used. In that case, it is wasteful to call across the process boundary twice for every property or method invocation.

One optimization would be to load the object's type information yourself (with LoadRegTypeLib or LoadTypeLib) and use ITypeInfo::GetIDsOfNames (which is usually called from within IDispatch::GetIDsOfNames anyway, but in the other process).2 Because the type information would be loaded in your controller's process, you would avoid all the extra cross-process calls, and you could do all this before creating the object. There is no gain in calling IDispatch::GetTypeInfo at run time and performing the dispID lookups through the returned ITypeInfothis ITypeInfo is itself tied to an out-of-process object, and as a result, you aren't saving anything.

Of course, an object might not have type information (Beeper1 in Chapter 14, for example) for you to load, in which case you have no choice but to call IDispatch::GetIDsOfNames after the object has been created, as is done in AutoCli.

With or without type information, there is still one more optimization—instead of mapping the name to a dispID before each call, map all the names in the controller script before execution begins, and keep the dispIDs in a cached table. You will then not only avoid two function calls (potentially across a process boundary) for each invocation, but you will eliminate all redundant calls to GetIDsOfNames. Later versions of Visual Basic use this sort of technique to improve run-time performance.

Whichever operation is being performed—property get, property set, or method call—your program ends up with a dispID for the method or property being invoked. Let's see how each different operation appears in a call to IDispatch::Invoke. In AutoCli, the actual call to Invoke happens through CApp::Invoke, which centralizes the IDispatch call as well as exception handling:


HRESULT CApp::Invoke(DISPID dispID, WORD wFlags, DISPPARAMS *pdp
, VARIANT *pva, EXCEPINFO *pExInfo, UINT *puErr)
{
HRESULT hr;
§

if (NULL==m_pIDispatch)
return ResultFromScode(E_POINTER);

hr=m_pIDispatch->Invoke(dispID, IID_NULL, m_lcid, wFlags
, pdp, pva, pExInfo, puErr);

§
}

This function is called from specific cases within the WM_COMMAND handling code (for the menu items) in AutoClientWndProc. This procedure declares as stack variables the various structures that we'll need to make the Invoke call:


DISPID          dispID, dispIDParam;
DISPPARAMS dp;
VARIANTARG va;
EXCEPINFO exInfo;
UINT uErr;
HRESULT hr;

Let's look at each Invoke case separately.

A Property Get

Retrieving a property is about the easiest thing for an automation controller to accomplish because it involves no arguments to pass to Invoke and is concerned only with the return value of the property:


    hr=pApp->NameToID(OLETEXT("Sound"), &dispID);

if (FAILED(hr))
break;

SETNOPARAMS(dp);
hr=pApp->Invoke(dispID, DISPATCH_PROPERTYGET
, &dp, &va, &exInfo, NULL);

In this code, AutoCli is finding the dispID of the Sound property (which is why this doesn't work in a language other than English or the neutral language) and passing that dispID to Invoke with the flag DISPATCH_PROPERTYGET to identify the operation. The return value comes back in the va parameter, in which va.lVal will have the sound. (AutoCli makes a string out of this sound and displays that string in its client area.) You'll notice that we must still pass a DISPPARAMS structure to Invoke here, but because there are no arguments, the structure is empty. The macro SETNOPARAMS, which you will find in the sample code file INC\INOLE.H, stores the appropriate NULLs and zeros in the structure using a more general macro, which is named SETDISPPARAMS:


#define SETDISPPARAMS(dp, numArgs, pvArgs, numNamed, pNamed) \
{\
(dp).cArgs=numArgs;\
(dp).rgvarg=pvArgs;\
(dp).cNamedArgs=numNamed;\
(dp).rgdispidNamedArgs=pNamed;\
}

#define SETNOPARAMS(dp) SETDISPPARAMS(dp, 0, NULL, 0, NULL)

These macros are simply a convenient way to save tedious typing. Note that if a property involves indices into an array, the controller can pass those indices in DISPPARAMS as arguments to the property get.

A Property Put

Setting a property to a new value is a little more complex than retrieving its current value. As we learned in Chapter 14, a controller must pass the new value for the property in a VARIANTARG structure in DISPPARAMS, and that one argument must be named with DISPID_PROPERTYPUT. Functions such as ITypeInfo::Invoke will enforce this, returning DISP_E_PARAMNOTFOUND, which is a difficult error to track down. (Additional arguments are allowed if the controller is passing array indices.)

In the following code, AutoCli again finds the dispID for Sound, packs up the new sound value—which is either the menu command value itself in wID or the value 0—in the va variable, puts that VARIANTARG into DISPPARAMS as a VT_I4, sets the rest of the DISPPARAMS structure, and then calls Invoke:


    hr=pApp->NameToID(OLETEXT("Sound"), &dispID);

if (FAILED(hr))
break;

VariantInit(&va);
va.vt=VT_I4;
va.lVal=(IDM_SETSOUNDDEFAULT==wID)
? 0L : (long)(wID);

dispIDParam=DISPID_PROPERTYPUT;
SETDISPPARAMS(dp, 1, &va, 1, &dispIDParam);

hr=pApp->Invoke(dispID, DISPATCH_PROPERTYPUT
, &dp, NULL, &exInfo, NULL);

You'll notice again that the controller has to set both the cArgs and cNamedArgs fields in DISPPARAMS to 1 in a property put operation. Also, because a property put has no return value, there is no reason to pass a VARIANT to Invoke for that purpose, which is why the fourth parameter to Invoke here is NULL.

A Method Call with No Arguments

When AutoCli invokes the Beep method, it executes almost exactly the same sequence of steps that were used in the property put case:


    hr=pApp->NameToID(OLETEXT("Beep"), &dispID);

if (FAILED(hr))
break;

SETNOPARAMS(dp);
hr=pApp->Invoke(dispID, DISPATCH_METHOD, &dp
, &va, &exInfo, &uErr);

In fact, if we used the dispID for Sound here instead of the one for Beep and changed the DISPATCH_METHOD flag to DISPATCH_PROPERTYGET, we'd have the equivalent of a property get. You can see from this why controllers that can't discern a property get from a method invocation have no trouble—they simply get the dispID and pass both DISPATCH_METHOD and DISPATCH_PROPERTYGET to Invoke. Even the return value is the same.

A Method Call with Arguments

The method call demonstrated in AutoCli is a degenerate one in which the situation is not complicated by those petty annoyances called arguments. Let's look at a hypothetical example of a method call for which arguments are involved. Let's assume we're working with Chapter 14's Cosmo as the automation server and that we want to invoke the Figure::AddPoint method, which takes two arguments. The hypothetical automation script the controller is running might have a line such as the following—in this case, we must parse the line and generate the right method call:


Figure.AddPoint 15000,43200

In parsing this code, we know first that Figure refers to a specific IDispatch pointer. We then find the name AddPoint, and given the dispinterface's type information, we know AddPoint is a method. In parsing the remainder of this line, we find two arguments—15000 and 43200—that we can assume are of type short. For the most part, the actual type is unimportant as long as the object can convert it to the correct type. In this example, we could send short, long, float, double, or BSTR types, and the object would probably be able to convert it. This is, of course, the lazy man's controller—the best thing you can actually do is to check the types that are present in the type information and try to match the type of your arguments to what the controller is expecting. That greatly increases the chances that the object will be able to use what you send.

So we have two short values that we now need to send to Invoke. We allocate two VARIANTARG structures and stuff them into DISPPARAMS:


//Assume dispID has "AddPoint" method ID.
//Assume xArg and yArg are the values we have from parsing.

DISPPARAMS dp;
VARIANTARG *pva;

pva=/(VARIANTARG*)malloc(sizeof(VARIANTARG)*2);

if (NULL==pva)
[memory error]

VariantInit(pva[0]);
pva[0].vt=VT_I2;
pva[0].iVal=xArg;
VariantInit(pva[1]);
pva[1].vt=VT_I2;
pva[1].iVal=yArg;

//cArgs=2, cNamesArgs=0
SETDISPPARAMS(dp, 2, pva, 0, NULL);
hr=pFigure->Invoke(dispID, DISPATCH_METHOD, &dp
, &vaRet, &exInfo, &uErr);

free(pva);
§

This code demonstrates the steps necessary to call the AddPoint method. Most likely a controller that does any kind of language parsing will have a generic function to create the correct argument list from any method signature and would not have specific code written as this is. In any case, this example—although limited—does illustrate passing more than one argument.

You may have a situation in which you need to pass the address of a variable for use as an out-parameter or for any other by-reference passing convention. If you do, the VARIANTARG you create will simply include the VT_BYREF flag and the right pointer value in the correct field of the VARIANTARG union. For example, if we passed xArg by reference in the preceding example, we'd set the argument this way:


VariantInit(pva[0]);
pva[0].vt=VT_I2 œ VT_BYREF;
pva[0].piVal=&xArg;

A Method Call with Optional Arguments

Calling a method that takes optional arguments requires that you actually send one VARIANTARG to the function for each optional argument. For those arguments that are not present, the VARTYPE must be set to VT_ERROR and the scode field must be set to DISP_E_PARAMNOTFOUND. For example, consider the standard document Close method that takes two optional parameters, SaveChanges and SaveFile. The code that appears in the controller can take one of any three forms, here using Cosmo's Figure object as an example:


Figure.Close
Figure.Close (False)
Figure.Close (True, "saveit.cos")

Let's assume that we encounter the first form of the call. We know from the object's type information that Close takes two optional arguments that we need to supply, but we know from parsing the running script that we have nothing to send it. We need to allocate two VARIANTARG structures anyway and fill them as follows:


pva=malloc(sizeof(VARIANTARG)*2);
§
VariantInit(pva[0]);
pva[0].vt=VT_ERROR;
pva[0].scode=DISP_E_PARAMNOTFOUND;
VariantInit(pva[1]);
pva[1].vt=VT_ERROR;
pva[1].scode=DISP_E_PARAMNOTFOUND;

The object will see these empty arguments and simply perform the default action for Close.

If we encounter the second form of the call, we know from parsing the line that we have one argument that we store in, say, fSave. We still have to allocate both VARIANTARGs, filling the first with actual data:


VariantInit(pva[0]);
pva[0].vt=VT_BOOL;
pva[0].bool=fSave; //fSave is a variable from parsing.
VariantInit(pva[1]);
pva[1].vt=VT_ERROR;
pva[1].scode=DISP_E_PARAMNOTFOUND;

The object will see that it should not save changes in this case and will ignore the second argument because it is irrelevant.

In the third form of the call, our parsing gives us a Boolean (fSave) and a string (pszFile). To pass the string we have to create a BSTR, which we free ourselves after Invoke returns:


VariantInit(pva[0]);
pva[0].vt=VT_BOOL;
pva[0].bool=fSave; //fSave is a variable from parsing.
VariantInit(pva[1]);
pva[1].vt=VT_BSTR;
pva[1].bstrVal=SysAllocString(pszFile);

[Set up DISPPARAMS and call Invoke.]
SysFreeString(pva[1].bstrVal);
§

A Method Call with Named Arguments

Dealing with methods that require named arguments is probably the hardest case in all the possible types of method calls because it involves more work with IDispatch::GetIDsOfNames. An example of this kind of method was given in Chapter 14, the FindRockBand method in the dispinterface of a database-type object:


[id(7)] long FindRockBand(int cMembers, BSTR LeadGuitar, BSTR BassGuitar
, BSTR Percussion)

When this line of ODL script was compiled into the object's type information, the arguments were given member IDs of 0, 1, 2, and 3, based on the order listed in the function signature. Now let's imagine a line of code in the automation controller's script in which these named arguments are not necessarily given in the same order:


id=Database.FindRockBand(3, BassGuitar="Lee", LeadGuitar="Lifeson"
, Percussion="Peart")

When we call GetIDsOfNames to find the dispID for FindRockBand, we have to pass the names BassGuitar, LeadGuitar, and Percussion to get their member IDs as well. This makes the call to GetIDsOfNames more complicated because you must create an array of string pointers to each name (method and arguments) as well as allocate an array of DISPIDs, one for each method and argument. Then you can retrieve the dispIDs:


//Assume we know we have 4 names (1 method, 3 arguments).
LPTSTR *ppsz;
DISPID *rgDispID;

ppsz=(DISPID *)malloc(sizeof(LPOLESTR)*cNames);
ppsz[0]=pszMethod; //Points to "FindRockBand" from parsing
ppsz[1]=pszArg1; //Points to "BassGuitar," the first parsed
ppsz[2]=pszArg2; //Points to "LeadGuitar," the second parsed
ppsz[3]=pszArg3; //Points to "Percussion," the third parsed

pDispID=malloc(sizeof(DISPID)*cNames);
pDatabase->GetIDsOfNames(IID_NULL, ppsz, cNames, lcid, pDispID);

On return, the pDispID array will contain the numbers 7 (dispID of FindRockBand), 2 (member ID of BassGuitar), 1 (member ID of LeadGuitar), and 3 (member ID of Percussion).

When we call Invoke, we have to allocate four VARIANTARG structures (three names plus the argument cMembers, which will have the value 3) and stuff them into the DISPPARAMS structure. Each string, of course, will have to be allocated as a BSTR. Assuming we've created the argument structures, we will fill DISPPARAMS as follows:


dp.cArgs=4;
dp.rgvarg=pva;
dp.cNamedArgs=3;
dp.rgdispidNamedArgs=&(pDispID[1]); //Skip method name!

The object will see that it has been sent named arguments and will use rgdispIDNamedArgs to determine which argument is which in the rgvarg array. For a controller, this means that you must absolutely match the order of the rgdispIDNamedArgs array with the order of VARIANTARG structures in rgvarg. Failure to do this will cause, well, mayhem. The order in which the arguments appear in the object's type information and the order in which they appear in the actual running script are both completely irrelevant: all that matters is that you precisely identify which argument is in what element of rgvarg.

2 This technique will not work for ITypeInfo::Invoke because you'd need the object's custom interface in order to call the function. The interface might not be exposed and probably would not marshal anyway. So it's not an option.