Monday, October 19, 2009

Extended Interfaces (#import)


3 4



Extended Interfaces (#import)


Given a type library, the compiler's #import directive generates, on the fly, a set of artifacts representing the type library's content, which make the objects and interfaces contained in the type library accessible to C++ client code. This by itself is a very useful feature. In the old days, C++ programmers had to manually invoke tools that would build these artifacts for them. If you were developing the project represented by the type library and the client access code simultaneously, things could easily get out of sync and access violations would result: clients would make a call to method A, but instead end up calling method B, and other fun problems. Through its automatic tie-in with the compilation process, #import solves all this. But it can do more than that: it can extend the binary compatibility contract between object and client, the interface, both syntactically as well as semantically. Let's examine these extensions in detail.



UUID Type Binding



Interface and coclass IDs are attached directly to their respective types. The #import directive uses the compiler's uuid attribute for __declspec to accomplish this. The ID of any interface or coclass so adorned can then be read using the __uuidof keyword. Have you ever wondered how CComQIPtr can work even when you omit the second template instantiation parameter? When provided, this parameter indicates the ID of the interface given as the first instantiation parameter. How can operator = be implemented if we don't know what interface ID to query for? The answer lies in the use of __uuidof. Both smart pointer template varieties rely on this feature.2



Exceptions



Every nonlocal method or property in a COM+ interface must return an HRESULT, indicating, among other things, invocation success or failure. As a rule, you must check the return value of each COM+ interface method invocation and act appropriately if the call failed. But such recoveries often mean merely that you terminate the execution of your client code and propagate the error to your caller. Doing this in-line with every method call can be tedious, error-prone, and make for hard-to-read source code. Wouldn't it be nice if COM+ methods could throw exceptions on failure, just like C++ functions? Well, now they can: by default, #import generates high-level method and property wrapper functions that extend the semantics of the raw method. One such extension features the addition of code that tests the return code for failure and generates an exception of type _com_error if failure was detected. The exception object then contains the error code.



Be sure to remember, however, that while an exception generated by a high-level wrapper function can be propagated to a C++ caller, it must never be allowed to cross an interface boundary. Have a look at Chapter 1, "Error Handling," for techniques that you can use to preserve the power of the exception-programming model without compromising the ease of its application.



Return Values



As I already mentioned, every nonlocal method or property in a COM+ interface must return an HRESULT. While an IDL parameter attribute retval exists and is encoded in type libraries, this is of no use to callers who use the low-level, binary compatibility interface class to access methods and properties. Another semantic extension performed by #import's high-level wrappers changes the method signature so that it moves that parameter designated with retval in the type library into the return value position. The HRESULT is then hidden, but the wrapper still throws an exception containing this value if it is a failure code.



As an added bonus, a smart pointer, which is itself returned by value, wraps this return value when its type is that of an interface pointer. That means that you can ignore the return value if you like, and you will not create a leak. The smart pointer is constructed on return from the wrapper in a manner that permits the compiler to perform return value optimization3, and so the underlying object or proxy does not experience an extraneous AddRef/Release pair of calls. These remarks do not, however, apply to other [out] parameters and you should pass the return value of the & operator of a smart pointer object in their positions.



Finally, let's take a look at the declaration and implementation of such a wrapper:





//�Created�by�Microsoft�(R)�C/C++�Compiler�Version�12.00.8472.0�(5e97cf11).
//
//�c:\program�files\microsoft�visual�studio\myprojects\returndemo\debug\
//�ReturnDemo.tlh
//
//�C++�source�equivalent�of�Win32�type�library�ReturnDemo.tlb
//�compiler-generated�file�created�03/10/00�at�20:24:04�-�DO�NOT�EDIT!

#pragma�once
#pragma�pack(push,�8)

#include�<comdef.h>

namespace�RETURNDEMOLib�{

//
//�Forward�references�and�typedefs
//

struct�/*�coclass�*/�Foo;
struct�__declspec(uuid("339e317c-42fe-4020-ab15-27a87a7e07e5"))
/*�dual�interface�*/�IFoo;
struct�__declspec(uuid("2980513a-412f-42aa-8bb4-dda58235163e"))
/*�dual�interface�*/�IBar;
struct�/*�coclass�*/�Bar;

//
//�Smart�pointer�typedef�declarations
//

_COM_SMARTPTR_TYPEDEF(IFoo,�__uuidof(IFoo));
_COM_SMARTPTR_TYPEDEF(IBar,�__uuidof(IBar));

//
//�Type�library�items
//

struct�__declspec(uuid("60047dbe-0276-4e07-94bb-aceae99843d6"))
Foo;
����//�[�default�]�interface�IFoo

struct�__declspec(uuid("339e317c-42fe-4020-ab15-27a87a7e07e5"))
IFoo�:�IDispatch
{
����//
����//�Wrapper�methods�for�error-handling
����//

����IBarPtr�GetBar�(�);

����//
����//�Raw�methods�provided�by�interface
����//

����virtual�HRESULT�__stdcall�raw_GetBar�(
��������struct�IBar�*�*�Result�)�=�0;
};

struct�__declspec(uuid("2980513a-412f-42aa-8bb4-dda58235163e"))
IBar�:�IDispatch
{};

struct�__declspec(uuid("eba409ab-1f62-4ee3-a19a-a914eb16bc44"))
Bar;
����//�[�default�]�interface�IBar

//
//�Wrapper�method�implementations
//

#include�"c:\program�files\microsoft�visual�studio\myprojects\returndemo\debug\
ReturnDemo.tli"

}�//�namespace�RETURNDEMOLib

#pragma�pack(pop)

//�Created�by�Microsoft�(R)�C/C++�Compiler�Version�12.00.8472.0�(5e97cf11).
//
//�c:\program�files\microsoft�visual�studio\myprojects\returndemo\debug\
//�ReturnDemo.tli
//
//�Wrapper�implementations�for�Win32�type�library�ReturnDemo.tlb
//�compiler-generated�file�created�03/10/00�at�20:24:04�-�DO�NOT�EDIT!

#pragma�once

//
//�interface�IFoo�wrapper�method�implementations
//

inline�IBarPtr�IFoo::GetBar�(�)�{
����struct�IBar�*�_result;
����HRESULT�_hr�=�raw_GetBar(&_result);
����if�(FAILED(_hr))�_com_issue_errorex(_hr,�this,�__uuidof(this));
����return�IBarPtr(_result,�false);
}





Smart Pointers and [in, out] Parameters


Smart pointers interact well with parameters in COM+ interface methods or properties whose directional attribute is either [in] or [out]: the smart pointer can be passed by value in the former case (its operator Interface* will take care of the implicit conversion), and the return value of operator & can be passed in the latter, ensuring that no leak occurs by overwriting an existing non-NULL value in the encapsulated raw pointer. The [in, out] directional attribute, however, is an entirely different story: the smart pointers' design resists these semantics because it requires giving callees control over the encapsulated interface pointer. The [in, out] directional attribute, however, calls for this kind of control.

The ATL templates give you a solution for this problem: pass the address of the encapsulated interface pointer—that is, write &ciSmart.p instead of &ciSmart. The _com_ptr_t class, however, resists all attempts at temporarily disabling its protective umbrella. Your best bet is to assign it to a type-compatible template instantiation of CComPtr, use the preceding trick of passing the encapsulated interface pointer, and then assign the result back to _com_ptr_t after the call.





Syntactic Properties



Accessor and mutator methods can be designated as properties in an IDL interface or type library. But again, to consumers of interface classes molded in the image of the binary interface contract such an attribute is of no relevance. C++ code going after the low-level interface class sees the raw accessor and mutator (also called get and put) functions. Enter the property attribute for __declspec: it instructs the compiler to convert all access to a data member to accessor and mutator method invocations or both instead. (No memory is reserved for such a syntactical data member.) The #import directive generates such a property declaration for each property found in a type library interface, and (when the property has an interface pointer type) has the high-level accessor wrapper function return this value wrapped by a smart pointer, as is the case with all return values. Array syntax is supported as well, where successive indices are passed to the accessor and mutator as
subsequent parameters. Exceptions are thrown on accessor and mutator invocation failure as expected.



The syntactic property feature has sparked some debate in the community of seasoned C++ programmers, where it may at times cause confusion rather than serve as a programming model simplification. Accessing a data member and having such access converted to method invocations by the compiler behind the scenes does appear somewhat—shall we say magical?—at first. But let's remember that this feature is all about ease of implementation, convenience, readability, and maintainability. Take a look at these equivalent code fragments and decide what you would rather write.





HRESULT�hResult;
int�nValue;

if�(FAILED(hResult�=�piFoo->GetMyValue(&nValue)))
����return�hResult;

nValue�+=�7;

if�(FAILED(hResult�=�piFoo->PutMyValue(nValue)))
����return�hResult;



or





ciFoo->MyValue�+=�7;



I cheated a little here, in that I took advantage of the exception semantics at the same time that I illustrated the property syntax. The former would have been available separately, even without using the property syntax. In any case, at best we are talking about one declaration and three other lines of code or else nested function calls without the property syntax. Therefore, four lines against one says that syntactic properties are a good thing.



Take a look once again at the code fragment in Listing 2-1. Let's rewrite this fragment using all techniques we have surveyed.





const�MyLib::IFooPtr�ciFoo(__uuidof(MyLib::Foo));
int�nMagic�=�ciFoo->Magic;

std::vector<int>�cMagicList;
cMagicList.push_back(nMagic);

if�(nMagic�<�CONST_MAGIC)
����return;

ComputeMagic(cMagicList);
ciFoo->Magic�=�cMagicList.front();



That's 8 lines of code compared to the original 24. But look at the difference: not only is it less, it is now also readable. The developer can finally focus on the task at hand, instead of struggling with COM+ plumbing every step of the way. This is what smart pointers are all about.



No comments:

Post a Comment