Friday, July 13, 2012

Summary: Sample illustrating how a C++ code can access a collection of objects returned by a .NET application.

Say you need to make a legacy C++ app call a web service. Since C++ is not very friendly to anything SOAP, the easiest option could be to write a helper .NET app, which would make SOAP calls, and expose it to your C++ app as a COM object via COM interop. This approach is rather straightforward, but it will get more complicated if your .NET COM object needs to pass collections to your C++ app. Here is how you can do it.

Disclaimer: There is no sample project and the code snippets below are incomplete, but they should be sufficient enough for a person familiar with C++ and C# to come up with the final solution. Also, no VB.NET samples, but C# snippets should be easy to follow and convert to VB.NET.

Step 1: Create a C# project for your .NET COM component.
I assume you already know how to do this, but if not, you can start with these articles:

Make sure to build a class library (DLL) and check the Register for COM interop option under the project's Build settings. Also, if you do not want to expose every public class, method and property from your assembly (e.g. if it calls a web service, you would not want the Visual Studio-generated proxy classes to be exposed as COM objects), uncheck the Make assembly COM-Visible option in the Assembly Information dialog box (to open this dialog, select the Application tab in the project properties and click the Assembly Information... button).

Step 2: Implement the primary COM component.
Your C++ app will use this COM component's methods to get data. The articles reference in the previous step explain what you need to do, but here are the main points:

You need to define an interface and implement a class derived from this interface (component class).

Your C++ app will only see the methods declared in the interface.

Both the interface and the component must be marked with unique GUIDs.

By default, ProgID of a .NET COM component is derived from the namespace and class names; to override it mark your component with an explicit ProgID.

I prefer to keep both the interface and component code in the same file, but you can put them in different files.

Since our ultimate goal is to pass a collection of objects from C# to C++, I will first implement a method that returns a custom object, and then a collection of these custom objects. Obviously, C++ will not be able to access this object as is, so we need to expose it as another .NET COM object. As with our primary object, the C++ code will access this component via an interface. Keep in mind that C++ does not understand the concept of property, so each property will need a helper getter (and -- if needed -- setter) method.

Step 3. Implement a COM component holding your data.
Let's say your C# app wants to return user info to the C++ app. We'll define a .NET COM object that will hold user properties. In this example, I assume that the C++ code will only need to get object properties, so we will omit COM methods to set them. Notice that we can still set the properties in the C# code:

Now that we have a .NET COM wrapper around our user info structure, let's define methods that return a single user object, as well as a collection of user objects.

Step 4. Implement COM methods in .NET component.
For the purpose of this discussion, implementation of user data retrieval from the back end is not important, so I will abbreviate it. You may get data from a web service, database, Active Directory, or some LDAP store, it does not really matter here. But you will need some way of communicating errors from the C# code to the C++ app. There are multiple ways to do this. I will use an integer return value to communicate error or success code (with zero indicating success) and use a string parameter to pass error information.

Now, the fun part: how do we access these methods and handle returned objects from the C++ code?

Step 5: Build type library (TLB) file for .NET COM assembly.
The easiest way to incorporate .NET COM classes in the C++ assembly is by importing the type library. By default, the .NET class library project build does not generate the type library, so add the following to the project's Post-build event command line settings (under the project's Build Events tab):

When this line gets compiled, Visual Studio will generate a TLH file with proxy classes for you .NET COM components. The file is located in your user account's AppData\Local\Temp folder. You do not need to explicitly reference this file in the source code or project; Visual Studio will do this for you implicitly. Something to keep in mind: if you make a change to the .NET COM object's public members, you need to recompile the C++ source (or header) file containing the import statement; otherwise, Visual Studio will not see your changes.

Step 7: Create .NET COM object in C++ code.
You can create a COM object in a number of ways: via CoCreateInstance, using smart pointers, etc. Here is how you do it via plain old CoCreateInstance:

Notice how names of the generated proxy classes, GUIDs, etc match the names you use in the .NET code. If you are not sure what the names are, find the TLH file and see the definitions there. Again, keep in mind that you will make all calls via the interface.

Once you verify that you C++ program successfully crated the .NET COM object, you can invoke the methods.

Step 8: Call .NET COM methods from C++ code.
If you are not sure what the data types of the COM method parameters should be, check their definitions in the TLH file. Normally, Visual Studio does translation correctly between basic data types, i.e. .NET string values will correspond to BSTR strings, .NET DateTime will map to DATE, bool values will turn into VARIANT_BOOL, etc. In this example, I'm interested in the more complex data types used to pass user data collection.

The code here distinguishes between the (unexpected) errors encountered when making COM calls and (expected) application errors encountered within the called .NET methods. The (unexpected) COM errors may occur due to problems in the environment. For example, if the .NET COM assembly is not properly registered or missing a dependency, the GetUser call will fail. COM errors are detected by checking the HRESULT return code (in this example, the hr value).

But the code in the .NET COM method can encounter an error, too. It can be an unexpected (read, system) or expected (read, application) error. To handle all errors gracefully, I recommend trapping all logic in .NET COM methods within exception handlers. If a method encounters an expected error condition (e.g. the specified user is not found), it should return an appropriate error code (there must be a agreement between the C++ and C# code on the meaning of the error codes) and an optional error message. In case of unexpected exception (e.g. network error when the .NET code calls the backend), the .NET code should set an appropriate error message and return a generic error code (for different ideas on error handling and communication, read my Dissecting Error Messages article in the Dr. Dobb's magazine). Here is an abbreviated example of error handling:

if (FAILED(hr))
{
// We could not even call the GetUser method.
// Prosess HRESULT value and get COM error info.
// Log retrieved error message.
// Handle error condition.
}
else
{
// At the very least, we know that we successfully
// called the GetUser method. But the method itself
// may have returned an error code and/or message.
if (nErrCode == 0)
{
// Assuming that 0 code means success,
// we can now acess properties of the pIUser
// object via the IMyUser interface methods.
// Do not forget to release this interface
// when it is no longer needed.
}
else
{
// At this point we know that we were able to
// call the GetUser method, but there was some
// error or problem within this method.
switch (nErrCode)
{
// Handle various error codes.
}
if (::SysStringLen(bstrErrMsg) > 0)
{
// Process error message (log it, or whatever)
// Remeber to free it when it's no longer needed.
::SysFreeString(bstrErrMsg);
}
}
}

I will not show how to use the pIUser object, because it is no different from any other COM object implemented in C++ or any other language.

The most confusing part involves accessing a collection (in our example, this collection is returned from a successful GetUserList call). To make it work, you need to do the following:

Define a variable of the type SAFEARRAY* (pointer to SAFEARRAY). You will pass the address of this variable (pointer to a pointer) to the method returning your collection.

Upon successful .NET COM call, check the data type of the SAFEARRAY members. If the method returns an array of objects (exposed as .NET COM interfaces), it's most likely that each member of the SAFEARRAY variable will hold an element of the type of VARIANT, IDispatch, or IUnknown interface. Once you detect the correct data type, convert it to a proper interface (in our case, IMyUser interface).