These days, software engineering involves a lot of flexibility. We have different programming paradigms, component object models, cooperating languages, even different binary standards. Many Windows-based developers would probably say that component software development began with COM. Others might say DLLs (and exported functions). A few might even say Windows controls. In a way, they're all right.
One of the challenges of working within even modern frameworks is the cost of development and maintenance time (not to mention run time) in handling all this flexibility. Several years ago, in order to obviate these costs, I wrote a C-API for data exchange between COM enumerators (instances implementing one or more of the IEnumXXXX family of interfaces) and Windows controls. I've updated part of that API to present here.
C++ development has recently (and wisely) swung towards the STL model of compile-time development. Whilst STL is no real help in binary interoperability, it is extremely useful for the implementation of binary interoperable components (and for pretty much every other kind of C++ software development that you can think of). STL has become extremely popular because it supports reuse (in a lightweight manner not more hefty hierarchies), maintainability, and very importantly, efficiency (or at least it does when used correctly). I've embraced STL in a big way: I have created the STLSoft project (http://stlsoft.org/) over the last couple of years in order to apply STL techniques to a broad range of technologies and operating systems (including Win32, UNIX, ATL, MFC, COM, WTL, XML, and .NET) in a robust and efficient manner. Two of the STLSoft subprojects, COMSTL (http://comstl.org/) and WinSTL (http://winstl.org/), contain components for manipulating COM objects and windows controls. I will show how collaboration between these components can provide an interesting alternative to the data exchange C-API.
C-API in C
Let's first look at the C-API. C++ aficionados might well wonder why anyone would bother writing a COM API in C. There are two answers. First, I like to write in C sometimes because it keeps the true understanding of the mechanics of COM to the fore in the brain. (Anyone who thinks that one can just use frameworks and never get into the guts has not written any real COM programs.) The second is that, when creating software that may be reused (in source form as well as binary), it is a good idea not to disenfranchise those developers who still prefer to exist in the C world. (Also, C affords you the opportunity to change the contents of the vtable at run time. It's very useful when you need it, but to be used very sparingly, and is not for the faint hearted.)
These observations notwithstanding, I acknowledge the masochism required to create such an API in C, and in your reading the code presented here. I would suggest that you persevere, mainly for the illumination previously alluded to.
Listing 1 shows the API flags, typedefs, structures, and function declarations. Skipping over what looks like a rather complex set of structures for the moment, we see there are two functions. The second one of these, ListExchange_PutItems(), has a manageable three arguments. They represent the window handle of the control, the enumerator interface, and flags, respectively. Hence, to populate, say, a list box with the contents of an IEnumVARIANT, one would call:
ListExchange_PutItems(hwndListBox, penVar, SYLXF_RESETCONTENT);
The SYLXF_RESETCONTENT flag causes the list control to be emptied prior to populating with the enumerator contents. Hence, you can populate a list control with the contents of several enumerators, thus:
ListExchange_PutItems(hwndComboBox, penVar1, SYLXF_RESETCONTENT); ListExchange_PutItems(hwndComboBox, penOleStr, 0); ListExchange_PutItems(hwndComboBox, penBSTR, 0);
That seems nice and straightforward. But how does it work? Naturally, the answer relates to the structures and the other function. In fact, if we look at Listing 2, we can see that ListExchange_PutItems() is actually implemented in terms of ListExchange_PutItems_Base(). Arrays of EnumeratorHandler structures and ControlHandler structures are defined and passed to ListExchange_PutItems_Base(). These structures describe the COM enumerators and windows controls that will be understood by the API. You can see that ListExchange_PutItems() understands the IEnumString, IEnumBSTR, IEnumVARIANT, IEnumGUID, and IEnumUnknown interfaces and the list-box, combo-box, list-view, and (multiline) edit controls that, for most requirements, more than suffices. (Note that there are two IEnumBSTR and IEnumGUID interfaces handled. This is because this API was defined and used with those Synesis-defined interfaces before the Microsoft interfaces of the same name were released; hence the need to support both. However, this dual support causes no problems since the interfaces are identically defined.)
The handlers are reasonably straightforward. The control handler contains the window class (an identifying type that all windows will provide via the GetClassName() API function), and functions to add an item and to reset the contents. The enumerator handler contains the Interface Identifier (IID) and functions to reset the enumerator, acquire an item's value (which it does via calling the Next() interface method), and to release an item's value. Because the functions operate with the Enum2WndItem item union and have an opaque handle (void*) for the value type (both defined in Listing 1), the API can be expanded to handle any enumerated value type simply by appropriately defining matching handler functions.
You may call ListExchange_PutItems() and use the predefined handlers, or set up your own handler arrays and directly call ListExchange_PutItems_Base(), which is where all the action happens (see Listing 3). The first few lines validate the arguments, followed by the first task of identifying the type of the window. The function iterates through the array looking for a matching control handler. If it fails to find one, then no further processing is done and the function returns a fail code (and writes to the thread's error object). Next is the test for the enumerator. If no enumerator is specified, then the control is simply emptied. It's not exactly orthogonal programming, but I just found it a real convenience (see Listing 6) to be able to clear the contents of any list control, no matter what its type, so this feature creep was let by. If an enumerator is specified, then it is tested against the enumerator handler information. If recognized, the control is cleared when requested note that this is not done earlier since a failure in a specified enumerator would change the list without populating it, and it is not generally a good idea to have a failing API effect changes if it can be avoided and then the enumeration is conducted.
Enumeration begins with resetting the enumeration point. The enumerator handler's pfnReset function is passed the enumerator interface pointer, which calls the IEnumXxxx:: Reset() method on the interface. Since all IEnumXxxx enumerator interfaces have the same general definition, and identical Reset() and Skip() method definitions, we can simply define one handler, EnumXXXX_Reset(), to be used for all interfaces, as can be seen in Listing 4; available online (the choice to cast to IEnumString is arbitrary). Inside the loop, the enumerator's Next() method is called within the enumerator handler's pfnNextItem function, passing in the instances of Enum2WndItem and LPVOID. If the function call returns S_OK, then the value is passed to the control handler's pfnPutItem function, and is thus added to the control. The value is then cleared (with pfnClearItem), so that any allocation in the enumerator handler's pfnNextItem function can be released. If the value returned from Next() is S_FALSE, then there are no more items available (and the return code is changed to S_OK before breaking out of the loop because S_FALSE is not meaningful to callers of the API). Any error return codes will cause the loop to terminate and will be passed back to the caller.
Note that the enumeration loop retrieves and inserts one item at a time. This is necessary because items from an arbitrary enumerator could be of any size. While it is certainly possible to cater for retrieving multiple items at a time, it would complicate the implementation of the handlers to a degree that I determined would not be warranted. (The efficiencies gained from reduced round-trips to COM enumerators are associated with marshalling across apartment/process/machine, and most enumerators from which one would wish to retrieve will likely be in the calling apartment. If not, marshal-by-value is also commonly used.) Also, all of the standard window handlers cause each string item to be added after the previous one, which covers most cases, but may not be what you want.
That's the main function. What remains is to see how the handlers are implemented. Listing 4 (available online) shows a selection of the enumerator handlers, and Listing 5 shows some of the control handlers. I won't go into too much detail as I've already discussed their required semantics. There are, however, a few points of interest that I'll briefly discuss.
First, the standard controls have to do conversions of the text to ANSI if the windows are not Unicode (determined by the calls to IsWindowUnicode()); there is no need to do this for the list view because, like all the Common Controls, it can work with both ANSI and Unicode messages. Second, the edit control handler works by moving the selection to the end of the contents, pasting the value text, repeating the selection, and pasting CR-LF. This is done in two parts to remove the need to concatenate the value and CR-LF, but it is conceivably open to race conditions (you'd have to write an atomic version if this was a possibility in your application). The rest of the implementations are fairly obvious, except the IEnumUnknown handler, which is discussed in the sidebar titled "DISPID_VALUE" (available online).
C-API in STL
ListExchange_PutItems() is a good solution for handling a variety of cases, whether for easy prototyping where one may wish to change list control types, or for production code that needs to handle multiple enumerator interfaces. However, using it constrains you to the enumerator and control types understood by the provided handlers, which may not cover your particular needs, or requires you to define your own, which is a fair amount of effort.
It is also a bit difficult to modify the items as they go into the list controls. This would require using special enumerator and control handler functions to change the value. The API does cater for that by defining the value type to be void * and not LPCOLESTR, and by providing a user-supplied parameter into ListExchange_PutItems_Base() that is passed through to the control handler's pfnPutItem but it's not a trivial effort to use it in this way.
The C-API was written a long time before STL became popular, and there are now alternatives if you prefer to be in the STL world entirely. Perhaps you only need to populate one kind of control and from one kind of enumeration interface, or you want to modify the contents of the retrieved items with adaptor functionals (function objects). Two STLSoft subprojects, COMSTL and WinSTL, provide components that, when used in concert, provide a very simple solution. If you have an IEnumString enumerator and you want to populate a list-box, the following code will do that:
LPENUMSTRING penStr = . . . // An IEnumString instance HWND hwndListBox = . . . // and a window to put it in // Declare an enumerator sequence for //IEnumString comstl::enum_simple_sequence< IEnumString , LPOLESTR , comstl::LPOLESTR_policy , LPCOLESTR , comstl::input_cloning_policy<IEnumString> , 5 // Next() five at a time > strings(penStr, true); // borrows the reference std::for_each(strings.begin(), strings.end(), winstl::listbox_back_inserter(hwndListBox));
Alternatively, if you have IEnumVARIANT and you want it to go into a combo box in reverse order:
comstl::enum_simple_sequence< IEnumVARIANT , VARIANT , comstl::VARIANT_policy , VARIANT const , comstl::input_cloning_policy<IEnumVARIANT> , 10 // Next() 10 at a time > strings(penVar, false); // eats the reference std::for_each(strings.begin(), strings.end(), winstl::combobox_front_insert er(hwndComboBox));
(It looks like a lot of code, but almost all of it is the parameterization of the enumerator sequence template. In real code, these are usually typedef'd.) As well as not needing to compile and link in the C-API, this has some other advantages. The sixth parameter to the template is a number, which specifies the number of items to acquire in each call to the enumerator's Next() method. Where this is important (e.g., when marshalling between apartments or between hosts), it allows you to increase efficiency by reducing round-trips by parameterizing the template appropriately.
You could also provide modification of the items before they go into the list control by applying an adaptor function (e.g., to set to uppercase) to the control inserter functional, as in:
template <typename F> struct upper_adaptor; template <typename F> upper_adaptor<F> make_upper_adaptor(F ); for_each( strings.begin(), strings.end(), make_upper_adaptor(combobox_front_inserter(hwnd ComboBox)));
Included in the online archive is the test program, shown in Figure 1. The dialog allows you to select one of five enumerator types (each of which are filled with sample data when they're created) and to select one of four list windows. Listing 6 shows some functions from the test program. MOCtlFns_Test_GetListWindow() is a helper function that retrieves the ID of the list control that is associated with a tab control item, and returns the corresponding window handle. It is used by MOCtlFns_Test_OnClear(), which demonstrates the use of ListExchange_PutItems() to clear out any list control, and by MOCtlFns_Test_OnPut(), which demonstrates the list population. MOCtlFns_Test_OnPut() obtains the appropriate COM enumerator from another function in the test program, MOCtlFns_Test_GetEnumerator(), which synthesizes an enumerator of the appropriate type (as determined by the "Source" radio group). In real code, you'd have received the enumerator from a source and would likely obtain the control from GetDlgItem(), so your code would be very succinct.
Listing 7 shows a solution based around the COMSTL enum_simple_sequence<> sequence class and the WinSTL control (standard and common) inserter functionals. Clearly, there is a lot of code to write, but this is just the demonstration program. In real-world scenarios, you'd use the C-API if you need this level of complexity, and the COMSTL/WinSTL components if you didn't.
Iterator concepts and IEnumXXXX::Clone()
A full description of the iterator concept is given at http://sgi.com/tech/stl/Iterators.html, and an in-depth treatment of the issues involved in writing iterators and sequences is given in my March 2003 WDN article (see References), so I'll try and be very brief here. An input iterator is something from which one may acquire its value only once. When copied, the ownership of the "range" is passed to the copy, making subsequent retrieval from the original invalid. A forward iterator is something from which one may take a copy and retrieve meaningful results from both copies.
COM programmers will recognize that the COM enumerator model describes a forward iterator semantic, by the Clone() method provided by all IEnumXxxx enumerator interfaces, which is defined to return a copy of the source enumerator that is at the same point in its enumeration as the source. However, it is legal for the Clone() method to fail, so were we to implement enum_simple_sequence's iterators as forward iterators, that could lead to trouble when it did so. Conversely, most enumerators succeed a call to Clone(), so were we to implement the iterators as input iterators, we would be restricting functionality in a large number of cases where that would not be necessary. Hence, the COMSTL enum_simple_sequence<> template takes a cloning policy so that users can select the desired iterator concept support based upon their needs. COMSTL provides several cloning policies, including:
- input_cloning_policy<> assumes Input Iterator semantics, and transfers ownership of the enumerator from the source iterator to the destination iterator.
- forward_cloning_policy<> assumes Forward Iterator semantics, and calls Clone() on the source iterator's enumerator to obtain one for the destination iterator. If this fails, the destination iterator is set to null, and the destination range [begin, end) is empty.
In our code, we have used the input_cloning_policy<> since in all cases, we only need one byte at the cherry of the enumerator ranges.
This article has examined the common but programmatically complex situation of transferring information from COM enumerators to windows controls. In a sense, we've looked at the two ends of the spectrum of modernity in our two methods. The old way, a C-API written in C, sacrifices some run-time efficiency for succinct client code and flexibility in the things that it can handle at run time. It has the slight disadvantage that it must work with one enumerated item at a time. It has a nice by-product that the list control can be cleared by a single function call without needing to worry about the window class. This is a piece of flexibility that can only be achieved at run time, since all windows are identified at compile time by their HWND only, and are distinguished by their window class that is obtained by a function call (GetClassName()).
The new way, using STL sequences and function objects, is more appealing to those (of us) who revel in intellectual elegance. It's also likely to be more efficient, since there are no run-time checks carried out on enumerator and list-control types, and it can retrieve multiple items in each call to IEnumXxxx::Next(). As is the case with generic (template) programming, you do have polymorphism, but it is compile time (i.e., in the type resolution and template parameterization). If this suits your needs, this is great. If you need to deal with multiple potential interfaces at run time, then you should stick with the C-API.
As with all things in software engineering, there is no absolute best approach. Each of these two approaches reflects an optimal solution to a particular set of circumstances. In one set of circumstances, you would use one; in a different set, the other. It's just a case of filling your bag of tricks with enough tricks.
Finally, you may be wondering whether I was going to discuss data exchange in the other direction? Well I'm sure it won't surprise you to learn that I've got both C-API and STL techniques for doing this, but that's another (longer) article.
Generic Programming and the STL, Matt Austern, Addison-Wesley, 1998.
STLSoft is an open-source organization applying STL techniques to a variety of technologies and operating systems. It is located at http://stlsoft.org/. COMSTL and WinSTL are subprojects, and are located at http://comstl.org/ and http://winstl.org/, respectively.
"Adapting Win32 Enumeration APIs to STL Iterator Concepts," Matthew Wilson, Windows Developer Magazine, March 2003.
"Inserter Function Objects for Windows Controls," Matthew Wilson, Windows Developer Network, November 2003.
Matthew Wilson is a software development consultant for Synesis Software, specializing in robustness and performance in C, C++, C#, and Java. Matthew is the author of the STLSoft libraries, and the forthcoming book Imperfect C++ (to be published by Addison-Wesley, 2004). He can be contacted via matthew @synesis.com.au or at http:// stlsoft.org/.