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.
Here is an abbreviated sample:
using System;
using System.Runtime.InteropServices;
namespace MyCompany
{
[Guid("11111111-1111-1111-1111-111111111111")]
[ComVisible(true)]
public interface IMyComObject
{
}
[Guid("22222222-2222-2222-2222-222222222222")]
[ProgId("MyCompany.MyComObject")]
[ComVisible(true)]
public class MyComObject: IMyComObject
{
}
}
Notice that we have not defined any methods, yet.
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:
using System;
using System.Runtime.InteropServices;
namespace MyCompany
{
[Guid("33333333-3333-3333-3333-333333333333")]
[ComVisible(true)]
public interface IMyUser
{
[ComVisible(true)]
int GetIdn();
[ComVisible(true)]
string GetUsername();
[ComVisible(true)]
bool GetIsDisabled();
}
[Guid("44444444-4444-4444-4444-444444444444")]
[ProgId("MyCompany.MyUser")]
[ComVisible(true)]
public class MyUser: IMyUser
{
[ComVisible(false)]
internal int Idn { get; set; }
[ComVisible(false)]
internal string Username { get; set; }
[ComVisible(false)]
internal bool IsDisabled { get; set; }
public MyUser()
{
Idn = 0;
Username = null;
IsDisabled = false;
}
[ComVisible(true)]
public int GetIdn() { return Idn; }
[ComVisible(true)]
public string GetUsername() { return Username; }
[ComVisible(true)]
public bool GetIsDisabled() { return IsDisabled; }
}
}
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.
using System;
using System.Runtime.InteropServices;
namespace MyCompany
{
[Guid("11111111-1111-1111-1111-111111111111")]
[ComVisible(true)]
public interface IMyComObject
{
[ComVisible(true)]
int GetUser
(
int idn,
ref IMyUser user,
ref string error
);
[ComVisible(true)]
int GetUserList
(
string mask,
ref IMyUser[] user,
ref string error
);
}
[Guid("22222222-2222-2222-2222-222222222222")]
[ProgId("MyCompany.MyComObject")]
[ComVisible(true)]
public class MyComObject: IMyComObject
{
[ComVisible(true)]
public int GetUser
(
int idn,
ref IMyUser user,
ref string error
)
{
int result = 0;
try
{
// GetUserFromBackend would retrieve data from
// backend and covert it to a MyUser object.
// We then need to cast the MyUser object as
// an IMyUser type.
user = GetUserFromBackend(idn) as IMyUser;
}
catch(Exception ex)
{
// GetErrorMessage would retrieve error
// messages from immediate and all inner
// exceptions.
error = GetErrorMessage(ex);
// Set proper return code indicating error.
result = GetErrorCode(ex);
}
return result;
}
[ComVisible(true)]
public int GetUserList
(
string mask,
ref IMyUser[] users,
ref string error
)
{
int result = 0;
try
{
// GetUserListFromBackend would retrieve data
// from backend and covert it to array of
// IMyUser objects (each element of the array
// will actually point to a MyUser object).
users = GetUserListFromBackend(mask);
}
catch(Exception ex)
{
// GetErrorMessage would retrieve error
// messages from immediate and all inner
// exceptions.
error = GetErrorMessage(ex);
// Set proper return code indicating error.
result = GetErrorCode(ex);
}
return result;
}
}
}
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):
call "$(DevEnvDir)..\..\VC\vcvarsall.bat" x86 >nul
tlbexp "$(TargetPath)" /out:"$(TargetDir)$(TargetName).tlb" /win32 /silent /nologo
Now, when you build the assembly, Visual Studio will also generate the TLB file along with the DLL. You will need the TLB file only for development.
Step 6: Import type library of the .NET COM assembly into the C++ project.
Add the
import statement to the source (or header) file(s) of your C++ project (you can use path relative to the C++ source or header file containing the
import statement):
// Set path to .NET COM project relative to the current file
#import "<path_to_project>\bin\Release\<assembly_name>.tlb" no_namespace named_guids raw_interfaces_only
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:
HRESULT hr = S_OK;
IMyComObject* pIMyComObject = NULL; // data type matches name of .NET interface
hr = CoCreateInstance
(
CLSID_MyComObject, // CLSID_ + name of .NET COM class
NULL,
CLSCTX_ALL,
IID_IMyComObject, // IID_ + name of .NET COM interface
(LPVOID *)&pIMyComObject
);
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.
Let's start with the call to get a single user object:
// Error message returned by .NET COM method
BSTR bstrErrMsg = 0;
// Error code returned by .NET COM method
long nErrCode = 0;
// .NET COM object (interface) holding user data
IMyUser* pIUser = NULL;
// hr (HRESULT type) will indicate whether COM call
// was successful
hr = pIMyComObject->GetUser
(
nIdn, // numeric identity (key) of the user (int type)
&pIUser, // returned .NET COM object
&bstrErrMsg, // passed by .NET COM method
&nErrCode // passed by .NET COM method
);
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).
Here is an abbreviated example:
// Array of objects returned from .NET COM method
SAFEARRAY* psaUserList = NULL;
hr = pIUser->GetUserList
(
bstrMask,
&psaUserList,
&bstrErrMsg,
&nError
);
// Process errors.
...
// The following assumes that the GetUserList call was successful.
// Data type of the elements of SAFEARRAY elements.
VARTYPE vt;
// Get data type of elements.
if (FAILED(hr = SafeArrayGetVartype(psaUserList, &vt)))
{
// Cannot detect data type of SAFEARRAY elements.
// Handle error and clean up.
}
// Check data type of SAFEARRAY elements.
if (vt == VT_VARIANT)
{
// Each element of SAFEARRAY is a VARIANT.
// Use a CComSafeArray wrapper class to process
// elements of the SAFEARRAY.
// Notice that it holds the VARIANT objects.
CComSafeArray<VARIANT> saUserList;
// Convert our SAFEARRAY into CComSafeArray.
if (FAILED(hr = saUserList.Attach(psaUserList)))
{
// Conversion attempt failed.
// Handle error and clean up.
}
// Iterate through all items in our collection.
for (int i=0; (ULONG)i<saUserList.GetCount(); i++)
{
// Use smart pointer to access user objects.
// Notice that we retrieve the original object
// from the appropriate member of the VARIANT
// data structure.
CComQIPtr<IMyUser> iUser = saUserList[i].punkVal;
// We ccan now acess properties of the iUser
// object via the IMyUser interface methods.
}
}
else if ((vt == VT_UNKNOWN) || (vt == VT_DISPATCH))
{
// All elements of SAFEARRAY are COM interfaces.
// Use a CComSafeArray wrapper class to process
// elements of the SAFEARRAY.
// Notice that it holds IUnknown pointers.
CComSafeArray<IUnknown*> saUserList;
// Convert our SAFEARRAY into CComSafeArray.
if (FAILED(hr = saUserList.Attach(psaUserList)))
{
// Conversion attempt failed.
// Handle error and clean up.
}
// Iterate through all items in our collection.
for (int i=0; (ULONG)i<saUserList.GetCount(); i++)
{
// Use smart pointer to access user objects.
// Notice that we retrieve the original object
// directly from the array element.
CComQIPtr<IMyUser> iUser = saUserList[i];
// We ccan now acess properties of the iUser
// object via the IMyUser interface methods.
}
}
else
{
// For other data types, add your own handling.
}
Now, was this fun or not?
See also:
Simplifying SAFEARRAY programming with CComSafeArray
SafeArray/idispatch issue
C# ATLCOM Interop code snipperts - Part 1
VARIANTS inside a SAFEARRAY