Migrating a Visual Basic 5.0 Component to Visual C++ 5.0

Michael Zonczyk
Microsoft Developer Network

July 1998

Summary: Explains how to create a Visual C++ version of a Visual Basic component. Includes discussion of the following:

In Phase 2, the Duwamish Books sample acquired a separate data access layer implemented as a Component Object Model (COM) server accessible by the progID (friendly registry name) of dbdal.CDataAccess. See the API Reference for the Data Access Layer (DAL).

One of the the goals for the Duwamish Books sample is to illustrate parallel implementations of the middle-tier software using several of the Microsoft® development languages in the Microsoft Visual Studio® toolset. The first installment toward this goal is here—a Visual C++® implementation of the data access component to match the Visual Basic® version.

In this article, I describe the issues, solutions, and observations I've encountered while developing the Visual C++ version in the footsteps of the Visual Basic version.

Ground Rules

Visual Basic's huge popularity owes a lot to its ease of use, and to how it leverages the broad range of Microsoft technologies while shielding the top-level programmer from the many complexities of Microsoft Windows® programming. With this in mind, the key ground rule for the C++ component is as follows:

Make the Visual Basic programmer's use of the Visual C++–implemented component as hassle-free as possible—the Visual C++ component must be able to stand in place of the former at both design time and run time.

I'll examine the practical effects of this rule while using the Visual C++ ActiveX® Template Library (ATL) COM technology to build the data access component. The Visual Basic-built component, developed first, serves as the point of reference both for run-time functionality and for the Visual Basic–friendly naming conventions accessible at design time.

To accommodate the Visual C++ perspective, I've added a secondary ground rule to make this task more interesting:

As much as possible, allow the C++ programmer to work in a natural Visual C++/ATL programming style.

The Strategy

Starting out, I have the Visual Basic source code for the server component in one hand and the Visual C++ ATL wizards in the other. My goal is to build an interchangeable server component that also looks the same when viewed under the Visual Basic design-time IntelliSense® feature.

I know that the ATL COM AppWizard requires me to fill in the name of classes and interfaces and that the ATL Object Wizard requires the names and attributes of methods and parameters as I declare the component. So, before we start: How do we get these details for the Visual Basic component? Naturally, Visual Studio provides the required tool—the OLE/COM Object Viewer extracts a facsimile of an Interface Definition Language (IDL) file from the typelib embedded in every COM DLL or EXE. (This indispensable tool is found in the Tools menu in Visual C++ , or in the Microsoft Visual C++ version 5.0 program group of the Windows Start menu.)

Here are the high-level steps I've decided on—which satisfy both ground rules:

  1. Using the ATL COM AppWizard, name the C++ class CDataAccess and the interface IDataAccess, and accept the rest of the component naming defaults.

  2. Examine the Visual Basic code and use the ATL Object Wizard to define each of the method signatures and properties. Use conventional C++ Hungarian notation for variable names. (Right-click your project's class view and click New ATL Object to access the ATL Object Wizard. Then, select Objects and double-click Simple Object.)

  3. Code functionality in C++ equivalent to that found in the Visual Basic component and test it.

  4. Use the OLE/COM Object Viewer to access the naming and attribute details exposed by the Visual Basic–built component, and generally to keep the Visual C++ project's IDL compatible with the Visual Basic implicit IDL as shown by the viewer.

  5. Implement the compile-time renaming mechanism so that the Visual C++ component works as expected with Visual Basic IntelliSense.

  6. Provide late-binding support for the class by adding the dbdal.CDataAccess version-independent progID key, with its CLSID value as a script block, into the dataacces.rgs registrar file.

Points of Interest

Symbol Renaming

Step 5 of the strategy—Implement the . . .renaming mechanism—requires you to use the IDL language feature cpp_quote(#include argnames.h) to cause MIDL, the IDL compiler, to propagate the include statement into the MIDL-generated project C++ dbdal.h header file.

The renaming macros in the argnames.h file are grouped into three areas, corresponding to the three different categories of symbols requiring translation: method arguments, library-related, and interface-related. Running the OLE/COM Object Viewer against the resulting dynamic-link libraries (DLLs), dbvbdal.dll and dbvcdal.dll, confirms that the renaming is successful.

Notice that for the library-related and CoClass-/interface-related symbols, our approach requires that we also supply redefinitions for a few additional symbols that are not in our source IDL. These are the globally unique identifier (GUID) symbols automatically constructed for us by the compiler. With these few additions our scheme is nearly complete.

Symbol renaming has a curious side effect when the redefinition target, "Recordset" (the Visual Basic program's choice for an argument name), coincides with the imported name of the namesake ActiveX Data Objects (ADO) CoClass. This problem surfaces when using the Visual C++ construct __uuidof(<CoClass name> )—the compiler no longer recognizes the "Recordset" name. To compensate for this name conflict, we've added a rename clause to our #import statement: : rename("Recordset", "adoRecordset"), which allows us to proceed using __uuidof(adoRecordset) in place of __uuidof(Recordset).

Supporting Run-time Errors (COM Exceptions)

The Visual Basic global Err object Raise method, used to propagate or originate error notification from a class object, has the following signature:

object.Raise number, source, description

In Visual C++, ATL implements something very similar—our component's static method CComCoClass<>.Error() with the following signature:

HRESULT Error( LPCOLESTR lpszDesc, const IID& iid, HRESULT hRes);

The slight difference in the data type of parameter two—a string in Visual Basic, and a GUID in Visual C++—affects the value of the source attribute of an Err/Exception object caught in the client. With the Visual Basic component, the value is what the programmer puts in the parameter, while with the Visual C++ component, the value is the registry name of the component—"dbdal" in our case.

IDL, Visual Basic, and GUIDS

A core element of the COM open standard is the Interface Definition Language (IDL). By using IDL, all external aspects for a component can be completely and unambiguously specified. In this article, we have highlighted the value of the OLE/COM Object Viewer tool because it gives us the ability to inspect the underlying IDL of a COM DLL or EXE. It should be obvious that a facility to control and edit a component's IDL specification during development is fundamental to writing components, which includes assigning a globally unique identifier (GUID) to each named element.

GUIDs are central to the Windows registry and the activation of components—for example, loading a COM DLL or starting a COM EXE in response to the New statement in Visual Basic.

As the Duwamish Books data access components were being developed, we considered synchronizing the GUIDs registered by either implementation. The intent: true binary GUID-level interchangeability, requiring only the (re-)registration of a preferred implementation DLL between client application restarts. This path was not taken, and we will explain why. Instead our components have so-called "progID-level" interchangeability—requiring rereferencing to the preferred component and recompilation of the client (but no code changes) between restarts. Note that neither action is necessary for late-binding script-based clients or if only affecting late-binding statements like CreateObject in Visual Basic—these use the shared nonunique progID string dbdal.CDataAccess.

Visual Basic shields the programmer from the IDL. Unlike developing under Visual C++, the Visual Basic environment totally controls the implicit IDL specification generated on behalf of the component developer. Also, Visual Basic cannot be stopped from (aggressively, from our standpoint) reassigning GUIDs at every recompilation, even for insignificant changes.

So we left out binary compatibility in Phase 2 because we want to avoid an ad-hoc solution. It is trivial to cut and paste GUIDS from an OLE/COM Object Viewer view of the completed Visual Basic DLL into the Visual C++ projects IDL and registry script files (dataacce.idl and dataacce.rgs)—and the reader is encouraged to try this. However, as we plan to bring other Microsoft development languages into the Duwamish fold, we will explore a more general answer to this GUID-synchronization issue in a later installment.

In case you're wondering if Visual Basic is capable of going the other way, using its "binary compatibility" feature and referencing the Visual C++-built DLL, this is not currently supported.

Minutia

Wrap-up

As this example shows, building a functional clone for our limited context is not particularly difficult—and with a little cleverness (for example, the argnames.h file) the Visual C++ programmer can use effective naming conventions throughout the IDL and C++ source code, while exposing whatever names the client prefers.

The Visual C++/ATL component is close enough, but not an exact clone of the one in Visual Basic. In fact, the interface, strictly speaking, is different—the Visual C++ methods return IDispatch pointers where Visual Basic returns _Recordset pointers. While transparent to our Visual Basic-built Duwamish Books client applications, a hypothetical scripting client would work with the Visual C++ server component (IDispatch) but fail with the Visual Basic server component (_Recordset). There are various other differences between our two CDataAccess component implementations—for example, interface attributes, such as oleautomation and pointer_default, or the entries placed into the registry. These variations are significant in the broad, COM sense, but not important to our specific client applications.