Tuesday, November 27, 2007

Modifying an array passed from a .NET Client to a COM Server

I'm encountering a situation where it would be nice to pass an array from a managed client (in c#) to a COM object (in-proc server) and have the COM object change the values in the array.  In my situation it's an array of strings but the principles apply to an array of base types as well.

 

In this case the COM object is an MFC dll.  There are several elements involved in setting up the communication.  These elements are:

 

IDL

MFC COM, at least in visual studio 2003, uses a combination of wizard generated IDL, macros and dynamic registration.  Since the focus of this post is COM Interop, I'll leave a discussion of MFC COM to another post (see technote 38 for helpful pointers).

 

The dispatch interface in the IDL needs to specify that the array is a SAFEARRAY of BSTRs.  e.g:


[id(4)] HRESULT MyArrayFunc([in, out] SAFEARRAY(BSTR) *saArray);

Managed strings are, by default, marshaled as BSTRs by the interop marshaler (not necessarily the same as the platform invoke default marshaling behavior).

SAFEARRAYs are arrays that know their size and number of dimensions.  They're an improvement over old C style arrays, which don't know their length.  The downside is that you have to use a bunch of API calls to access elements of the array (most importantly SafeArrayGetElement() and SafeArrayGetUBound()).

 

NATIVE CODE/C++/C

MFC uses dispatch maps to associate a native method with the declared methods of an interface.  Each interface method needs to be included in the dispatch map (BEGIN_DISPATCH_MAP/END_DISPATCH_MAP in your C++ source) via a DISP_FUNCTION (or DISP_FUNCTION_ID) macro.

 

COM Interop is extremely sensitive to the values set by DISP_FUNCTION_ID.  If the return type or parameter types are incorrectly specified then there is a good chance that a type exception will be thrown when the method is invoked!

 

To pass an array of strings, the parameter type must be set to VTS_VARIANT, e.g.:


DISP_FUNCTION_ID(CMyClass, "MyArrayFunc", dispidStrArrayFunc, MyArrayFunc, VT_I4, VTS_VARIANT)


NOTE: setting a return type of VT_HRESULT does not work, use VT_I4 (long) instead.

 

Inside the header file, declare the prototype (make sure it's public, vs2003 wizards seem to put it in a protected: section):

HRESULT MyArrayFunc(VARIANT &vArray)

 

Then in the source file, implement ala:


HRESULT CMyClass::MyArrayFunc(VARIANT &vArray)

{

AFX_MANAGE_STATE(AfxGetStaticModuleState());

// make sure it's a string array

if (V_VT(&vArray) != (VT_BYREF | VT_ARRAY | VT_BSTR))

AfxThrowOleDispatchException(1001, "Type Mismatch in Parameter. Pass a string array by reference");

// get the safearray from the variant

SAFEARRAY **ppsa = V_ARRAYREF(&vArray);

cout << "In StrArrayFunc()" << endl;


cout << "dimensions=" << SafeArrayGetDim(*ppsa) << endl;

// get lower and upper bounds

long lLBound, lUBound;

SafeArrayGetLBound(*ppsa, 1, &lLBound);

SafeArrayGetUBound(*ppsa, 1, &lUBound);

cout << "lower bound=" << lLBound << ", upper bound=" << lUBound << endl;

// access each element

BSTR bstrCurrent;

BSTR bstrNew;

CString curStr;

for (long i=lLBound; i <= lUBound; i++)

{

SafeArrayGetElement(*ppsa, &i, &bstrCurrent);

cout << "vArray[" << i << "]=" << CW2A(bstrCurrent) << endl;

SysFreeString(bstrCurrent);

bstrNew = SysAllocString(L"replaced");

HRESULT hr = SafeArrayPutElement(*ppsa, &i, bstrNew);

if (FAILED(hr))

goto error;

}

 

return S_OK;

error:

AfxThrowOleDispatchException(1003, "Unexpected Failure in StrArrayFunc method");

return 0;

}


MANAGED CODE/C#

Once a reference to the COMServer is added, an interop class will be created with methods defined in the dispatch interface.  According to the documentation the type library importer is supposed convert [in, out] SAFEARRAY(BSTR) *param to a ref string[].  Unfortunately that doesn't seem to be what happens; it gets imported as a System.Array.  This means there are a few extra steps to calling the unmanaged function and then reading the modified array.  e.g.,


MyCOMServer.MyClassClass mc = new MyClassClass();

String[] ar = new string[30];

for (int i=0; i < ar.Length; i++)

ar[i] = "str1";

Array a = (Array) ar;

mc.MyArrayFunc(ref a);

// NOTE: to get return value, have to cast back from the object passed in by reference...

string[] nAr = (string[]) a;

 


 

 

 

No comments :

Post a Comment