//
you're reading...
Programming Issues/Tips

Specifying SAFEARRAY parameters with COleDispatchDriver::InvokeHelper()

1. Introduction.

1.1 Recently, someone from the MSDN forum requested advise on a problem he faced while attempting to call a C# method (exposed as a COM method) from an unmanaged MFC application.

1.2 The C# method takes an array of integers by reference, e.g. :

bool SetArray([In][Out] ref Int32[] integer_array);

1.3 His C# class is based on an interface which is IDispatch-based. His client application is an MFC-based. He imported the C# class into the MFC-based client app as an MFC class generated from a type library.

1.4 The wizard-generated wrapper class for the C# class is derived from COleDispatchDriver. It will include all public methods of the C# class. The wrapper for the SetArray() method is listed as follows :

BOOL SetArray(SAFEARRAY * * integer_array)
{
  BOOL result;
  static BYTE parms[] = VTS_UNKNOWN ;
  InvokeHelper(0x1, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, integer_array);
  return result;
}

1.5 When he called this method, a message box appeared indicating that he parameter is incorrect.

1.6 This blog examines a problem with using COleDispatchDriver-based wrapper methods. The specific problem is with the inherent limitations on parameter type expressions available for the COleDispatchDriver::InvokeHelper() method.

2. C# Class and Client Code.

2.1 To help illustrate the problem I have written a C# class contained in a class library. This class library is then exposed as a COM server named TestCSServer.dll.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace TestCSServer
{
    [ComVisible(true)]
    [Guid("4A723AB8-92C3-4bfb-AF82-65E2D42BB2DA")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface ITestInterface
    {
        [DispId(1)]
        bool SetArray([In][Out] ref Int32[] integer_array);
    }

    [ComVisible(true)]
    [Guid("18C8F92E-BE11-431f-9741-01AEEBC2E09E")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("TestCSServer.TestClass")]
    public class TestClass : ITestInterface
    {
        public bool SetArray([In][Out] ref Int32[] integer_array)
        {
            int i = 0;

            // Modify the values.
            for (i = 0; i < integer_array.Length; i++)
            {
                integer_array[i] += 10;
            }

            return true;
        }
    }
}

By selecting the “Register for COM interop” option for the project’s properties, when the TestCSServer project compiled successfully, a type library TestCSServer.tlb is generated and is registered by the IDE.

2.2 The client is an MFC dialog-based application. As mentioned previously, the client code creates an MFC class from the type library of the C# class library. The wizard generates the following COleDispatchDriver-derived wrapper class for ITestInterface :

class CTestInterface : public COleDispatchDriver
{
    public:
	CTestInterface(){} // Calls COleDispatchDriver default constructor
	CTestInterface(LPDISPATCH pDispatch) : COleDispatchDriver(pDispatch) {}
	CTestInterface(const CTestInterface& dispatchSrc) : COleDispatchDriver(dispatchSrc) {}

    // Attributes
    public:

    // Operations
    public:

    // ITestInterface methods
    public:
	BOOL SetArray(SAFEARRAY * * integer_array)
	{
		BOOL result;
		static BYTE parms[] = VTS_UNKNOWN ;
		InvokeHelper(0x1, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, integer_array);
		return result;
	}

    // ITestInterface properties
    public:

};

2.3 The test code is encapsulated in the handler for a button. Here is the code :

void CTestMFCClient01Dlg::OnBnClickedButtonTestCallSetarray()
{
	// TODO: Add your control notification handler code here
	CTestInterface test_interface;
	HRESULT hr;

	if (test_interface.CreateDispatch(L"TestCSServer.TestClass") == FALSE)
		return;

	SAFEARRAY* pSafeArrayOfIntegers = (SAFEARRAY*)SafeArrayCreateVector(VT_I4, 0, 10);

	if (pSafeArrayOfIntegers == NULL)
	{
		return;
	}

	for (int i = 0; i < 10; i++)
	{
	  long lIndex[1];
	  long value = (long)i;

	  lIndex[0] = i;
	  SafeArrayPutElement(pSafeArrayOfIntegers, lIndex, (void*)&value);
	}

	test_interface.SetArray(&pSafeArrayOfIntegers);

	SafeArrayDestroy(pSafeArrayOfIntegers);
	pSafeArrayOfIntegers = NULL;
}

The test code creates an instance of the C# class as a COM object through its prog ID. The COM object is represented by an instance of the CTestInterface wrapper class. Thereafter it builds up a SAFEARRAY of 4-byte integers and then passes it to the wrapper’s SetArray() method. Soon after SetArray() is called, a message box indicating “The parameter is incorrect” is diaplayed.

3. The Source of the Problem.

3.1 There are 2 causes of the problem :

  • The wizard-generated wrapper for the SetArray() method.
  • Current limitations on parameter type expressions available for COleDispatchDriver::InvokeHelper().

3.2 The first problem is directly caused by the second. To understand this, observe the wrapper for the SetArray() method :

BOOL SetArray(SAFEARRAY * * integer_array)
{
  BOOL result;
  static BYTE parms[] = VTS_UNKNOWN ;
  InvokeHelper(0x1, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, integer_array);
  return result;
}

3.3 Note the “parms” variable. Its assigned value “VTS_UNKNOWN” indicates that the parameter for the SetArray() method is an IUnknown pointer.

3.4 The wizard-generated wrapper for SetArray() is obviously incorrect because we know that the parameter is an array of 32-bit integers. The problem is that the COleDispatchDriver::InvokeHelper() method is not able to accept SAFEARRAY parameters. There are no VTS_* constants that indicate a SafeArray parameter type. This is a limitation.

3.5 The problem is caught inside COleDispatchDriver::InvokeHelperV() which is called by COleDispatchDriver::InvokeHelper().

3.6 Inside COleDispatchDriver::InvokeHelperV(), the COleDispatchDriver class prepares to call IDispatch::Invoke() using the contained IDispatch pointer which has been instantiated to point to the COM-Callable-Wrapper (CCW) of the C# TestClass object.

3.7 As part of this preparation, COleDispatchDriver::InvokeHelperV() creates an array of VARIANTs for each parameter of SetArray(). It then proceeds to fill in the VARIANT parameters with values.

3.8 As it does so, it unwraps the variable arguments list “argList” based on information contained in “pbParamInfo”. Walking backwards through the stacktrace, we note that “pbParamInfo” originates from “parms” in the SetArray() wrapper.

3.9 Now for the SetArray() method, “parms” has been set to “VTS_UNKNOWN” which indicates that the corresponding argument in “argList” is to be interpreted as a pointer to an IUnknown interface. This means that the pointer to the pointer to the SAFEARRAY that was passed previously in CTestMFCClient01Dlg::OnBnClickedButtonTestCallSetarray() :

test_interface.SetArray(&pSafeArrayOfIntegers);

will be take to be a pointer to an IUnknown interface.

3.10 The VARIANT for the paramter that will be passed to SetArray() will thus be of variant type VT_UNKNOWN. The IUnknown pointer is the address of the pointer to the SAFEARRAY.

3.11 The error is caught when COleDispatchDriver::InvokeHelperV() calls IDispatch::Invoke() :

	// make the call
	SCODE sc = m_lpDispatch->Invoke(dwDispID, IID_NULL, 0, wFlags,
		&dispparams, pvarResult, &excepInfo, &nArgErr);

3.12 The returned SCODE value “sc” will be 0x80070057 (E_INVALIDARG).

3.13 There are 2 solutions that can be used to workaround the problem. These are explained in the next 2 sections.

4. Workaround 1 – Directly Call IDispatch::Invoke().

4.1 The first workaround is to bypass the CTestInterface COleDispatchDriver wrapper and directly call the IDispatch::Invoke() mehod. That is, do not use COleDispatchDriver::InvokeHelper().

4.2 The following is a sample code that demonstrates this :

void CTestMFCClient01Dlg::OnBnClickedButtonTestCallSetarrayWorkaround1()
{
    // TODO: Add your control notification handler code here
    CTestInterface test_interface;
    HRESULT hr;

    if (test_interface.CreateDispatch(L"TestCSServer.TestClass") == FALSE)
    {
      return;
    }

    SAFEARRAY* pSafeArrayOfIntegers = (SAFEARRAY*)SafeArrayCreateVector(VT_I4, 0, 10);

    if (pSafeArrayOfIntegers == NULL)
    {
      return;
    }

    for (int i = 0; i < 10; i++)
    {
      long lIndex[1];
      long value = (long)i;
      lIndex[0] = i;
      SafeArrayPutElement(pSafeArrayOfIntegers, lIndex, (void*)&value);
    } 	 	

    // Do not use CTestInterface::SetArray().
    //test_interface.SetArray(&pSafeArrayOfIntegers);
    // Instead, make the IDispatch::Invoke() method call directly.
    DISPPARAMS dispparams;
    VARIANTARG varArgument;
    VARIANTARG varResult;

    VariantInit(&varArgument);
    VariantInit(&varResult);

    // The argument for SetArray() is a reference array of 4-byte integers.
    V_VT(&varArgument) = (VT_BYREF | VT_ARRAY | VT_I4);
    // Make the argument variant point to the address of the
    // target safearray pSafeArrayOfIntegers.
    V_ARRAYREF(&varArgument) = &pSafeArrayOfIntegers;
    memset(&dispparams, 0, sizeof(DISPPARAMS));
    dispparams.cArgs = 1;
    dispparams.rgvarg = &varArgument;

    // Obtain the ID of the SetArray() method.
    OLECHAR FAR* rgszNames = L"SetArray";
    DISPID dispid = 0;
    EXCEPINFO excepinfo;

    (test_interface.m_lpDispatch) -> GetIDsOfNames
    (
      IID_NULL,
      &rgszNames,
      1,
      LOCALE_SYSTEM_DEFAULT,
      &dispid
    );

    // Finally making the call
    UINT nErrArg;
    hr = (test_interface.m_lpDispatch) -> Invoke
    (
      dispid,
      IID_NULL,
      LOCALE_SYSTEM_DEFAULT,
      DISPATCH_METHOD,
      &dispparams,
      &varResult,
      &excepinfo,
      &nErrArg
    );

    // Destroy the SAFEARRAY.
    SafeArrayDestroy(pSafeArrayOfIntegers);
    pSafeArrayOfIntegers = NULL;
}

4.3 The call will go through because we have personally ensured correct variant type for the VARIANT that is used to hold the reference to the SAFEARRAY that gets passed to the C# SetArray() method.

4.4 When the IDispatch::Invoke() method calls through, the SAFEARRAY will have its element values each incremented by a value of 10.

5. Workaround 2 –

5.1 The second workaround is to change the SetArray() method parameter type from “ref Int32[]” to “ref object”.

5.2 To demonstrate this, instead of modifying the current SetArray() method, I have defined a new method for ITestInterface, SetArrByObject() :

[DispId(2)]
bool SetArrayByObject([In][Out] ref object integer_array);

5.3 Listed below is a sample implementation :

public bool SetArrayByObject([In][Out] ref object integer_array)
{
  // Cast the "integer_array" object into an Int32[] array.
  Int32[] arr = (Int32[])integer_array;
  int i = 0;

  // Modify the values.
  for (i = 0; i < arr.Length; i++)
  {
    arr[i] += 10;
  }

  return true;
}

As can be seen, the code is very similar to that of SetArray(). The input parameter is now a reference to an object which is cast into an Int32 array. The values of the array are then modified in the same way.

5.4 This time, when we re-import the C# TestClass into the MFC client application project via the Add Class wizard, the following wrapper will be generated for SetArrayByObject() :

BOOL SetArrayByObject(VARIANT * integer_array)
{
  BOOL result;
  static BYTE parms[] = VTS_PVARIANT ;
  InvokeHelper(0x2, DISPATCH_METHOD, VT_BOOL, (void*)&result, parms, integer_array);
  return result;
}

Notice the “parms” variable. It is assigned “VTS_PVARIANT” which is indeed correct. It indicates that the parameter for “SetArrayByObject()” is a pointer to a VARIANT.

5.5 The following is a sample calling code :

void CTestMFCClient01Dlg::OnBnClickedButtonTestCallSetarrayWorkaround2()
{
  // TODO: Add your control notification handler code here
  CTestInterface	test_interface;

  if (test_interface.CreateDispatch(L"TestCSServer.TestClass") == FALSE)
  {
    return;
  }

  SAFEARRAY* pSafeArrayOfIntegers = (SAFEARRAY*)SafeArrayCreateVector(VT_I4, 0, 10);

  if (pSafeArrayOfIntegers == NULL)
  {
    return;
  }

  for (int i = 0; i < 10; i++)
  {
    long lIndex[1];
    long value = (long)i;
    lIndex[0] = i;
    SafeArrayPutElement(pSafeArrayOfIntegers, lIndex, (void*)&value);
  }

  // Declare a VARIANT in which we store the SAFEARRAY.
  VARIANT var;
  // Note that "var" simply holds the SAFEARRAY.
  // This SAFEARRAY must not be by reference.
  // It is the VARIANT (i.e. "var") that must be
  // by reference.
  VariantInit(&var);
  V_VT(&var) = (VT_ARRAY | VT_I4);
  V_ARRAY(&var) = pSafeArrayOfIntegers;

  test_interface.SetArrayByObject(&var);

  SafeArrayDestroy(pSafeArrayOfIntegers);
  pSafeArrayOfIntegers = NULL;
}

Note well an important point about this workaround. The “pSafeArrayOfIntegers” SAFEARRAY that is stored inside the “var” VARIANT is not stored by reference (the variant type for “var” does not contain a VT_BYREF flag unlike that demonstrated in Workaround 1). It is the “var” VARIANT that is passed by reference to the “SetArrayByObject()” method.

 

 

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: