none
MFC Application pass SAFEARRAY to .NET COM component RRS feed

  • Question

  • Hi all,


    I'm new to MFC, COM and .NET (well, new to windows programming, basically), and I have the following situation:

    1. A .NET GUI component project exported to COM (C++/CLI, built in VS2008),

        registered using 'regasm.exe myLib.dll /tlb',

        implements a IMylib interface (InterfaceType::Dual),

        contains a method which takes in a parameter 'array<int>^ value',

    2. A MFC application(built in VS2003) project that needs to use the .NET component,

        add above .NET component by "Add class->MFC Class From ActiveX Control" (not by #import),

     

    Problem:

    MFC wrapper class generated failed to compile, error C2143: syntax error : missing ';' before 'constant'

    	unsigned char MyMethod(long _id, SAFEARRAY * _values)
    	{
    		unsigned char result;
    		static BYTE parms[] = VTS_I4 VTS_NONE;
    
    		InvokeHelper(0x60020003, DISPATCH_METHOD, VT_UI1, (void*)&result, parms, _id, _values);
    		return result;
    	}
    

    problem (as I understand) is:

    .NET component 'array<int>^ value' is mapped to SAFEARRAY* in COM, but SAFEARRAY* is not properly mapped to a VTS_*

     

    my questions:

    1. How to modify the wrapper class?

    2. To use the .NET GUI component, is above way (in MFC project Add class->MFC Class From ActiveX Control) correct? If not, what's the correct way?

     

    Thanks in advance!

     

    Regards,

    Wade

    Tuesday, March 1, 2011 10:08 AM

Answers

  • Hello Wade,

     

    1. >> .NET component 'array<int>^ value' is mapped to SAFEARRAY* in COM, but SAFEARRAY* is not properly mapped to a VTS_*

    1.1 There are several limitations inherent in the wizards which are due mainly to unexpected user requirements or code complexities which cannot be generalized. 

    1.2 In such cases, hand-coding wizard-generated code becomes necessary. Fortunately for your particular case there is a way around this problem which is suggested below.

     

    2. >> How to modify the wrapper class?

    2.1 I suggest modifying the wrapper function for MyMethod() to the following :

     unsigned char MyMethod(long _id, /*SAFEARRAY **/ VARIANT* _values)
     {
      unsigned char result;
      static BYTE parms[] = VTS_I4 /*VTS_NONE*/ VTS_VARIANT ;
      InvokeHelper(0x60020004, DISPATCH_METHOD, VT_UI1, (void*)&result, parms, _id, _values);
      return result;
     }

    2.1.1 Change the type of the "_values" parameter from SAFEARRAY* to VARIANT*.

    2.1.2 Change parms[] to equal VTS_I4 VTS_VARIANT.

    2.2 Thereafter, you may call the MyMethod() wrapper in the following way :

     VARIANT var;
     SAFEARRAY* pSafeArrayOfInt = NULL;
     
     // create a SAFEARRAY of VT_I4 and store the pointer 
     // in pSafeArrayOfInt.
     ...
     ...
     ...
     
     VariantInit(&var);
     V_VT(&var) = VT_ARRAY | VT_I4;
     V_ARRAY(&var) = pSafeArrayOfInt;
     CMylib.MyMethod(0, &var);
     
     ::SafeArrayDestroy(pSafeArrayOfInt);
     pSafeArrayOfInt = NULL;

     

    3. Pls give it a try and let us know how things turn out.

     

    - Bio.

     

    • Marked as answer by __Wade__ Friday, March 4, 2011 1:44 AM
    Wednesday, March 2, 2011 11:54 AM

All replies

  • Hello Wade,

     

    1. >> .NET component 'array<int>^ value' is mapped to SAFEARRAY* in COM, but SAFEARRAY* is not properly mapped to a VTS_*

    1.1 There are several limitations inherent in the wizards which are due mainly to unexpected user requirements or code complexities which cannot be generalized. 

    1.2 In such cases, hand-coding wizard-generated code becomes necessary. Fortunately for your particular case there is a way around this problem which is suggested below.

     

    2. >> How to modify the wrapper class?

    2.1 I suggest modifying the wrapper function for MyMethod() to the following :

     unsigned char MyMethod(long _id, /*SAFEARRAY **/ VARIANT* _values)
     {
      unsigned char result;
      static BYTE parms[] = VTS_I4 /*VTS_NONE*/ VTS_VARIANT ;
      InvokeHelper(0x60020004, DISPATCH_METHOD, VT_UI1, (void*)&result, parms, _id, _values);
      return result;
     }

    2.1.1 Change the type of the "_values" parameter from SAFEARRAY* to VARIANT*.

    2.1.2 Change parms[] to equal VTS_I4 VTS_VARIANT.

    2.2 Thereafter, you may call the MyMethod() wrapper in the following way :

     VARIANT var;
     SAFEARRAY* pSafeArrayOfInt = NULL;
     
     // create a SAFEARRAY of VT_I4 and store the pointer 
     // in pSafeArrayOfInt.
     ...
     ...
     ...
     
     VariantInit(&var);
     V_VT(&var) = VT_ARRAY | VT_I4;
     V_ARRAY(&var) = pSafeArrayOfInt;
     CMylib.MyMethod(0, &var);
     
     ::SafeArrayDestroy(pSafeArrayOfInt);
     pSafeArrayOfInt = NULL;

     

    3. Pls give it a try and let us know how things turn out.

     

    - Bio.

     

    • Marked as answer by __Wade__ Friday, March 4, 2011 1:44 AM
    Wednesday, March 2, 2011 11:54 AM
  • Hi Bio Liong,

    Thank you very much for your reply. It works!

    I also did a bit searching and find the VARIANT wrap around SAFEARRAY method. I took your suggestion, but used CComSafeArray.

    VARIANT var; 
    CComSafeArray<int >* pCComSafeArray;
    // Create and init CComSafeArray
    VariantInit(&var); V_VT(&var) = VT_ARRAY | VT_I4; V_ARRAY(&var) = (SAFEARRAY*)(*pComSafeArray->GetSafeArrayPtr());
    CMylib.MyMethod(0, &var); pCComSafeArray->Destroy()

     

    And I have a follow up question:

    Since I did not modify the .NET code, its assembly, and type library generated are not changed, so it's expecting a SAFEARRAY*.

    But VARIANT* is not SAFEARRAY*, so what really happens here?

    My best guess is, there's additional handling for VARIANT*:

        if parameter type match

            send parameter through

        else if parameter type is VARIANT

            if VARIANT content type match

                 send VARIANT content through

        else type mismatch exception

     

    If my guess is correct, where does this take place? Wish someone could shine some light on this, or some pointers to where I can find such info. Beginner struggling with .NET COM Interop here.

    Thanks again Bio.

     

    Regards from HarborFront,

    Wade
    Thursday, March 3, 2011 1:11 PM
  • Hello Wade.

     

    1. >> Thank you very much for your reply. It works!

    You are most welcome and congratulations.

     

    2. ... But VARIANT* is not SAFEARRAY*, so what really happens here?

    Some backgrounder :

    2.1 MFC invokes your ActiveX's methods through a process known as late binding.

    2.2 Late binding is the act of programmatically constructing a function call (together with the inclusion of parameters and the receipt of return values) at runtime.

    2.3 Late binding is made possible by the IDispatch interface. It is perhaps the most generic and flexible of all COM interfaces.

    2.4 The IDispatch method which performs the late bound function call is Invoke().

    2.5 When IDispatch::Invoke() is called, all parameters to the target method must be wrapped inside a VARIANT. See the MSDN documentation for IDispatch::Invoke() (http://msdn.microsoft.com/en-us/library/ms221479.aspx).

     

    3. How Does MFC Perform Late-Bound Method Calls on an ActiveX ?

    3.1 When you add an MFC Class from an ActiveX Control, a CWnd-derived class is generated for you to represent the ActiveX.

    3.2 This CWnd object internally contains a COleControlSite object (i.e. CWnd::m_pCtrlSite) which logically represents the physical window area on which the ActiveX control resides (the "site" of the ActiveX control, see http://msdn.microsoft.com/en-us/library/w9b4e0zd(v=vs.80).aspx).

    3.3 The COleControlSite object in turn contains a member COleDispatchDriver object (COleControlSite::m_dispDriver) which is the driving force behind late-bound method calls. See http://msdn.microsoft.com/es-es/library/fw39e08y(v=vs.80).aspx.

    3.4 At runtime, the COleControlSite object connects the COleDispatchDriver object with the IDispatch interface pointer of the IOleObject pointer of your ActiveX object.

    3.5 Provided that you build a debug version of your application, you can trace the method call from MyMethod() all the way down to the IDispatch::Invoke() method call to your ActiveX control object (the COleDispatchDriver::InvokeHelperV() method in oledisp2.cpp).

    3.6 You are right in figuring that additional processing is done to the individual parameters of MyMethod() before they are actually sent off to the target method in the server. It is inside the COleDispatchDriver::InvokeHelperV() method that the parameters to MyMethod() are analyzed and then wrapped into individual VARIANTs to serve as parameters to the late-bound method (as mentioned in point 2.5 above).

    3.7 COleDispatchDriver also uses variable argument list access functions extensively (e.g. va_arg, va_end, va_start) so that the MFC wizard can generate wrapper methods for the methods of the ActiveX regardless of the number of parameters each method uses. Notice that the wizard-generated methods all end up using InvokeHelper() to perform their work.

    3.8 The first parameter for MyMethod(), _id, which is a long, is wrapped into a VARIANT of type VT_I4. The information for the fact that _id is of type long is taken from the VTS_I4 that we saw in MyMethod(). The "lVal" field of the VARIANT is taken from the value of "_id" itself. You can see this in action in the source codes for the COleDispatchDriver::InvokeHelperV() method which is partially listed below (code taken from oledisp2.cpp) :

     case VT_I4:
      pArg->lVal = va_arg(argList, long);
      break;

    3.9 Now the second parameter for MyMethod(), the SAFEARRAY, has already been put into a VARIANT. Hence the VARIANT to be passed as parameter to the IDispatch::Invoke() method, is the VARIANT that we passed to MyMethod() :

     case VT_VARIANT:
      //VARIANT is always passed by ref
      *pArg = *va_arg(argList, VARIANT*);
      break;

    3.10 However, note that MyMethod() itself will not be able to tell if wrong parameters types are passed to IDispatch::Invoke(). In fact not even COleDispatchDriver::InvokeHelperV() can tell this. Inside the ActuveX wrapper methods (e.g. MyMethod()), as long as the VTS_* values match with their corresponding input parameters, all will go fine.

    3.11 It is the server itself that can tell when a parameter type mismatch has occurred. If this should happen, the IDispatch::Invoke() method will return DISP_E_TYPEMISMATCH. In such a case, the last parameter to Invoke(), i.e. puArgErr (a pointer to an unsigned int), will return the index of the parameter that mismatched. See MSDN documentation for IDispatch::Invoke() for more details.

     

    Hope the above will be helpful to you for a start. To understand more about COM and IDispatch, I heartily recommend "The Essence of COM : A Programmers Workbook" by David S Platt. It's a little old but it's really good.

     

    - Bio.

     

    Thursday, March 3, 2011 4:43 PM
  • Hi Bio,

    Thank you so very much for your timely and informative reply!

    I followed your advice and trace the debug from the MFC application in VS2003, and see where COleDispatchDriver comes into play.

    Thank you for your recommendation on a good reference book. I do feel lacking of good systematic reference, and wandering from website to website. I am referencing a book on MFC programming with some ActiveX pointers(an old book I found from somewhere), but I find that more of a guide to the Visual Studio wizards. Another book I was recommended was "inside ole" by Kraig Brockschmidt, however, being a 15-year old book it's hard to find.


    Thank you again, Bio. I mark your first reply as answer, as it's for the original question.

     

    Best Regards,

    Wade

    Friday, March 4, 2011 1:43 AM