//
you're reading...
.NET/COM/ActiveX Interop

Passing a Managed Array to a C++ ActiveX via a Pointer.

1. Introduction.

A few days ago, a member from the MSDN Visual C++ Forum posted a question presenting the following problem :

1.1 He has an ActiveX control which contains the following method :

[id(1), helpstring("method CopyToBuffer")] SHORT CopyToBuffer(SHORT* src);

1.2 The twist is that the forum member wants the method to receive an array of short values. He figured that the “src” parameter can be treated as a C/C++ array.

1.3 To his surprise, when he referenced the ActiveX in a C# WinForm application, he found that the method signature was :

public virtual short CopyToBuffer(ref short src);

He had expected the method signature to be something like :

public virtual short CopyToBuffer(short [] src);

1.4 Faced with this declaration, he tried to use the ActiveX control in the following way in C# :

private void buttonTest_Click(object sender, EventArgs e)
{
  short[] shortsArray = new short[100]; 

  for (short i = 0; i < shortsArray.Length; ++i)
    shortsArray[i] = i; 

  axTest.CopyToBuffer(ref shortsArray[0]);
}

1.5 This blog post explores the problem with misunderstanding a method signature as declared in point 1.1. This blog also studies what is the proper way to declare such a method given the original intension of treating the parameter as an array. This blog will also explore ways to workaround the problem.

2. SAFEARRAY – the correct type to use.
2.1 If the “src” parameter is intended to be an array of short values, then it needs to be declared as a SAFEARRAY of shorts.

2.2 The following is how CopyToBuffer() must be declared in the ActiveX project’s ODL file :

[id(1)] short CopyToBuffer(SAFEARRAY(short) src);

The original declaration :

[id(1)] short CopyToBuffer(short* src);

indicates that “src” is a pointer to one single short value. It is not interpreted by the MIDL compiler as an array of shorts.

2.3 The declaration for CopyToBuffer() in its C++ class’ .h file should be :

SHORT CopyToBuffer(VARIANT& src);

instead of :

SHORT CopyToBuffer(SHORT* src);

2.4 The Dispatch Map entry for “CopyToBuffer” should be :

BEGIN_DISPATCH_MAP(CTestCtrl, COleControl)
  DISP_FUNCTION_ID(CTestCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
  DISP_FUNCTION_ID(CTestCtrl, "CopyToBuffer", dispidCopyToBuffer, CopyToBuffer, VT_I2, VTS_VARIANT)
END_DISPATCH_MAP()

instead of :

BEGIN_DISPATCH_MAP(CTestCtrl, COleControl)
  DISP_FUNCTION_ID(CTestCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
  DISP_FUNCTION_ID(CTestCtrl, "CopyToBuffer", dispidCopyToBuffer, CopyToBuffer, VT_I2, VTS_PI2)
END_DISPATCH_MAP()

We must use a VARIANT& type as the parameter for CopyToBuffer() because there is no VTS_ type for SAFEARRAY.

3. Effects of Above Changes.

3.1 With the above-mentioned changes put in place, the signature for the CopyToBuffer() method will now be :

short CopyToBuffer(System.Array src);

3.2 The C# code buttonTest_Click() must also be changed :

private void buttonTest_Click(object sender, EventArgs e)
{
  short[] shortsArray = new short[100];   

  for (short i = 0; i < shortsArray.Length; ++i)   
    shortsArray[i] = i;   

  axTest.CopyToBuffer(shortsArray);
}

4. Possible Workaround for a C++ Client App.

4.1 In this section I will demonstrate one possible workaround (for a C++ client). This workaround will help to illustrate why there is a problem when a C# client is involved (explained in section 5 below).

4.2 Assuming the original method definition :

SHORT CopyToBuffer(SHORT* src);

In order for the CopyToBuffer() to receive a pointer to an array of short values, when a caller invokes CopyToBuffer(), marshaling must be avoided.

4.3 Once marshaling gets involved, whatever array of shorts that may have been prepared by the client calling function will be processed such that only the first array item will be marshaled across to the ActiveX CopyToBuffer() method.

4.4 Let me show this concept with an unmanaged C++ application, e.g. :

void CMFCClient01Dlg::OnBnClickedButtonTest()
{  
  // TODO: Add your control notification handler code here  
  std::vector<short> vecShort;    

  for (int i = 0; i < 100; i++)  
  {   
    vecShort.push_back(i);  
  }    

  m_TestActiveX.CopyToBuffer(&(vecShort[0]));
}

The above is a code-snippet from an MFC dialog-based application. The ActiveX’s CopyToBuffer() method is called with a pointer to an array of shorts.

4.5 Amazingly, the CopyToBuffer() method could access the entire (non-SAFEARRAY) array of shorts. However, this is only possible if the CopyToBuffer() method was called in the UI thread. This ensured that the call occurred in the same apartment as the ActiveX control and so marshaling was not involved in the call. The method receives a direct pointer to the actual array.

4.6 If the call was made in a separate thread, marshaling will kick in and the method will once again receive a pointer to one single short.

4.7 Workaround not withstanding, this is still not an elegant solution. It is certainly not the correct way to pass an array to a COM method. If cross-apartment calls are required, the workaround will be defeated.

5. Why This Will Not Work for a C# Client.

5.1 The problem with calling an ActiveX method in a C# application is that interop marshaling is now involved. This is so regardless whether the call is made within the same apartment. This interop marshaling is performed when the AxHost object for the ActiveX control invokes a method call.

5.2 As part of the interop marshaling process, managed objects which are to be used as parameters will have their values copied into unmanaged memory. This is where the type library for the ActiveX is used to determine the parameter types. The types determine how much memory is to be allocated. If the parameter type is a reference to a short, then memory large enough for a short type is allocated and its address is passed as parameter.

5.3 The AxHost will then forward the call to the method using the actual IDispatch pointer of the ActiveX control.

5.4 This is the reason why only the first value of the C# array is visible to the CopyToBuffer() method. The interop marshaler made sure of this (to our chagrin).

5.5 The only way the CopyToBuffer() method will work properly for a C# client is to have its parameter changed to a SAFEARRAY of shorts in the ODL file. The generated type library will then reflect this information so that the AxHost will allocate a SAFEARRAY and copy all values from the C# array to this SAFEARRAY. The SAFEARRAY will then be passed to the CopyToBuffer() method.

6. Possible Workaround for C#.

6.1 There is a possible but albeit convoluted workaround for a C# client application.

6.2 The following is a synopsis :

6.2.1 Create a DLL with an exported function as follows :

short __stdcall CopyToBuffer(/*[in]*/ VARIANT varIDispatch, /*[in]*/ SAFEARRAY* pSafeArrayOfShorts);

6.2.2 This exported function takes the following parameters :

– VARIANT varIDispatch

This VARIANT will be used to contain the IDispatch pointer of the ActiveX control.

– SAFEARRAY* pSafeArrayOfShorts

This SAFEARRAY will be passed from the C# code.

6.2.3 This API will internally unwrap the VARIANT to obtain the IDispatch pointer of the ActiveX control which is contained within the Axhost object on the C# window form.

6.2.4 It will also unwrap the SAFEARRAY created in the C# code to obtain a pointer to the the array of shorts.

6.2.5 We then proceed to use the IDispatch pointer to make a (late-bound) call to the CopyToBuffer() method of the ActiveX control. The return value from the method call will be returned to the C# code.

6.2.6 The central idea is to make a direct late-bound call to the ActiveX control’s IDispatch pointer passing along an array of shorts disguised as a pointer to one single short value. We have seen that this will work as long as marshaling is not involved.

6.3 Note well, however, that this workaround has its limitations (see section 8 below) and is non-standard. My advise is to stick to standard techniques and use SAFEARRAYs.

7. Sample Code for C# Workaround.

7.1 Listed below is the complete code for the CopyToBuffer() API :

short __stdcall CopyToBuffer(/*[in]*/ VARIANT varIDispatch, /*[in]*/ SAFEARRAY* pSafeArrayOfShorts)
{  
  short shRet = 0;    

  if (V_VT(&varIDispatch) != VT_DISPATCH)  
  {   
    // Invalid argument.   
    return -1;  
  }    

  short* pArrayOfShorts = NULL;  
  SafeArrayAccessData(pSafeArrayOfShorts, (void**)&pArrayOfShorts);    

  IDispatch* pIDispatch = V_DISPATCH(&varIDispatch);  
  DISPPARAMS dp;  
  VARIANTARG Arg;  
  VARIANTARG vr;    

  VariantInit(&vr);
  VariantInit(&Arg);  
  V_VT(&Arg) = (VT_BYREF | VT_I2);  
  V_I2REF(&Arg) = pArrayOfShorts;  

  memset(&dp, 0, sizeof(DISPPARAMS));  
  dp.cArgs = 1;  
  dp.rgvarg = &Arg;    

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

  pIDispatch -> GetIDsOfNames  
  (  
    IID_NULL,                   
    &rgszNames,   
    1,           
    LOCALE_SYSTEM_DEFAULT,                    
    &dispid           
  );   

  // Finally making the call  
  UINT nErrArg;  
  HRESULT hr = pIDispatch -> Invoke ( dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dp, &vr, &excepinfo, &nErrArg );    

  SafeArrayUnaccessData(pSafeArrayOfShorts);  
  pSafeArrayOfShorts = NULL;  
  pArrayOfShorts = NULL;    

  if (V_VT(&vr) == VT_I2)  
  {   
    shRet = V_I2(&vr);  
  }      

  return shRet;
}

7.2 And the C# declaration :

[DllImport("HelperAPIs.dll", CallingConvention = CallingConvention.StdCall)]
private static extern Int16 CopyToBuffer ( object objIDispatch, [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_I2)] Int16[] arrayIfShorts );

7.3 A sample call in C# :

private void buttonTest_Click(object sender, EventArgs e)
{
  short[] shortsArray = new short[100];   

  for (short i = 0; i < shortsArray.Length; ++i)    
    shortsArray[i] = (short)(100 - 2 * i);   

  //axSomeComp.CopyToBuffer(ref shortsArray[0]);  
  CopyToBuffer((object)(axTest.GetOcx()), shortsArray);
}

8. Limitations.

8.1 The CopyToBuffer() API suffers the same limitation as the C++ workaround sample that I mentioned in section 4 : a call to CopyToBuffer() must be performed in the same apartment as the ActiveX control.

8.2 If CopyToBuffer() was called in another apartment (just try calling it in a separate thread), the workaround will not work. The following code will demonstrate this :

public partial class Form1 : Form
{
  [DllImport("HelperAPIs.dll", CallingConvention = CallingConvention.StdCall)]   
  private static extern Int16 CopyToBuffer ( object objIDispatch, [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VarEnum.VT_I2)] Int16[] arrayIfShorts ); 

  public Form1()   
  {   
    InitializeComponent();   
  }   

  private void buttonTest_Click(object sender, EventArgs e)   
  {   
    // Start a thread and call the ActiveX's CopyToBuffer()     
    // in that thread.     
    Thread newThread = new Thread(DoWork);     
    newThread.Start(axTest);   
  }   

  public static void DoWork(object obj)   
  {   
    // Cast "obj" to the ActiveX's type.     
    AxTestLib.axTestActiveX activex = (AxTestLib.axTestActiveX)obj;     
    short[] shortsArray = new short[100];     

    for (short i = 0; i < shortsArray.Length; ++i)     
      shortsArray[i] = i;     

    CopyToBuffer((object)(activex.GetOcx()), shortsArray);  
  }
}

9. Interesting Side Note.

9.1 Here is something interesting that you may want to note.

9.2 We must use the IDispatch pointer returned from the AxHost.GetOcx() method. This provides us with the IDispatch pointer of the actual ActiveX itself.

9.3 If the CopyToBuffer() API was called in the following way :

CopyToBuffer((object)axTest, shortsArray);

i.e. without a call to GetOcx(), the API will receive the IDispatch pointer to the AxHost object.

9.4 This makes alot of difference because then interop marshaling will once again take place and the ActiveX’s CopyToBuffer() method will again receive a pointer to one single short.

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

One thought on “Passing a Managed Array to a C++ ActiveX via a Pointer.

  1. Greate!
    Thanks very much!

    Posted by ScottQ | June 20, 2011, 2:39 am

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: