none
Failure Marshaling a VARIANT [out, retval] from C# to C++ via Interop RRS feed

  • Question

  • I have a situation where an COM interface is defined in a TLB that is implemented in a .NET component and called from C++ and .NET clients.  I have a need to control the return value to the C++ clients differently than the .NET clients, so I started down the path of a custom marshaler.  I can't get the C# Custom Marshaler to return a variant properly. 

    My C++ client gets a VARIANT back with invalid VT_TYPE and what looks like random memory.  The COM call returns S_OK, but the memory looks incorrect.  If anyone could help me figure out what my marshaler (Item 2 below) is doing wrong, I'd appreciate it.  I've included the TLB/IL for the Interop to provide more background information on the problem.

    1. The COM TLB and Interface

    To link my custom marshaler, I had to generate the PIA for the COM TLB, run it through ILDASM to get to the IL, then add the custom marshaling attribute.  Here is the COM IDL interface and the corresponding IL that I modified and reassembled:

    IDL

      [id(2), helpstring("Get components by a name")]
      HRESULT GetCompByName([in] BSTR componentName, [out, retval] VARIANT *components);

    IL

      .method public hidebysig newslot abstract virtual
              instance object
              marshal( custom ("NetServer.DispatchWrapperMarshaler, NetServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3858d17845491709",""))
              GetCompByName([in] string  marshal( bstr) componentName) cil managed
      {
        .custom instance void [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = ( 01 00 02 00 00 00 00 00 )
      } // end of method ILegacyInterface::GetCompByName

     

    2. Code in ICustomMarshaler.MarshalManagedToNative()

        public IntPtr MarshalManagedToNative(object ManagedObj)
        {
          System.Diagnostics.Trace.WriteLine("## MarshalManagedToNative");
          if (ManagedObj == null)
          {
            throw new ArgumentNullException("ManagedObj");
          }

          string str = "Hello World";
          IntPtr variant = IOP.Marshal.AllocCoTaskMem(16);
          IOP.Marshal.GetNativeVariantForObject(str, variant);

          return variant;
        }

    3. Client Code

    My client code is a console app that has CoInitialized into an STA apartment, creates the .NET object via smart pointer CreateInstance and then calls the GetCompByName method.  The VARIANT I get back has a bizarre .vt value and all the union values in the variant look like uninitialized memory.

    Here's my client code:

    int _tmain(int argc, _TCHAR* argv[])
    {
      CoInitialize(NULL);
      {
        ILegacyInterfacePtr spLegacy;
        HRESULT hr = spLegacy.CreateInstance(_T("NetServer.Session"));

        if(spLegacy)
        {

          VARIANT var;
          VariantInit(&var);

          hr = spLegacy->GetCompByName(_bstr_t(_T("Hello World")), &var);

          TCHAR szBuf[200];
          ZeroMemory(szBuf, sizeof(TCHAR)*200);
          wsprintf(szBuf, _T("Called GetCompByName: hr=0x%08X"), hr);
          OutputDebugString(szBuf);

          if(var.vt==VT_BSTR)
          {
            OutputDebugString(_T("VARIANT is a BSTR\n"));
          }

          VariantClear(&var);

        }
      }
      CoUninitialize();

      return 0;
    }

    Any help would be appreciated.

    Thanks,


    Brian R.

    Tuesday, March 27, 2012 4:31 PM

Answers

  • After submitting this concern to Microsoft for a technical support issue they've noted that a CustomMarshaler cannot be used because it does not support value type marshaling.  Because of the signature of GetCompByName, which passes a VARIANT, it can't be done this way.

    Also, using a different interface definition is not possible because .NET and COM alike cast the .NET server to the same interface. 

    Thanks to all that tried to help.


    Brian R.

    • Marked as answer by Brian R. _ Thursday, April 26, 2012 6:39 PM
    Thursday, April 26, 2012 6:39 PM

All replies

  • Hello Brian,

    1. If your intention is to return a VARIANT that contains a BSTR, there is no need to use a custom marshaler.

    2. Based on the definition of the GetCompByName() method, the ILegacyInterface interface would be something like the following :

        [TypeLibType(4288)]
        [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
        public interface ILegacyInterface
        {
            [DispId(2)]
            object GetCompByName(string componentName);
        }

    3. The GetCompByName() implementation could be something like the following :

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    [ClassInterface(ClassInterfaceType.None)]
    public class Session : ILegacyInterface
    {
        public object GetCompByName(string componentName)
        {
            string str = "Hello World";
            object objRet = (object)str;
            return objRet;
        }
    }

    4. The "str" string would be wrapped inside a VARIANT as a BSTR and returned to the C++ client code as such.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Wednesday, March 28, 2012 5:34 AM
  • It is not my intention to return a VARIANT that contains a BSTR.  This is just a very simple example that I could create to try to show the problem that I am seeing with the more complicated example.  What this method really returns in the legacy system is a VARIANT that contains a SAFEARRAY of IDispatch members.  I have the code to pinvoke and create the SAFEARRAY, but am having trouble getting this into a VARIANT and returned to the client.  My tactic was to simplify to get a simple case working, then move to the more complicated VT_ARRAY | VT_DISPATCH.

    The standard marshaler does fine for us, but forces us to change the return value from .NET depending on the caller.  TRying to determine the caller has been a losing game.  Per the current definition, if the caller is a C++/VB6 caller, we create an Array and wrap each component in a DispatchWrapper and insert into the array.  This forces the standard marshaler to return a VT_ARRAY|VT_DISPATCH.  However if a .NET client calls us, then they get an Array of DispatchWrappers and their code fails.  We don't have the option of modifying either caller.  We can't go back to writing this COM server in C++.

    So, it would be helpful if you could do one of the following:

    1. Help me figure out how to invoke and use the standard marshaler from my custom marshaler.  This would allow my custom marshaler to put the Array members in DispatchWrappers, then call the standard marshaler to do the real work.

    or

    2. Help me figure out what the proper IL changes are and how to marshal the ARRAY back as type VT_ARRAY|VT_DISPATCH.

    Again, I figured addressing this simpler example first would be a good helper so that I can figure out what I am doing wrong.

    Thanks


    Brian R.

    Wednesday, March 28, 2012 2:26 PM
  • Hello Brian,

    1. >> My C++ client gets a VARIANT back with invalid VT_TYPE and what looks like random memory.  The COM call returns S_OK, but the memory looks incorrect.  If anyone could help me figure out what my marshaler (Item 2 below) is doing wrong, I'd appreciate it...

    1.1. Before we begin to search for a solution, I want to highlight to you an interestng observation that may explain why DispatchWrapperMarshaler went wrong.

    1.2 The sections that follow will explain this in detail.

    2. What MarshalManagedToNative() Returns Is A Pointer to a VARIANT.

    2.1 Observe the MarshalManagedToNative() method :

        public IntPtr MarshalManagedToNative(object ManagedObj)
        {
          System.Diagnostics.Trace.WriteLine("## MarshalManagedToNative");
          if (ManagedObj == null)
          {
            throw new ArgumentNullException("ManagedObj");
          }
          string str = "Hello World";
          IntPtr variant = IOP.Marshal.AllocCoTaskMem(16);
          IOP.Marshal.GetNativeVariantForObject(str, variant);
          return variant;
        }

    2.2 Here, "variant" indeed contains a BSTR created from the managed string "str".

    2.3 What is returned is a pointer to a VARIANT.

    3. What the Client Code Receives.

    3.1 Observe the C++ client code :

    VARIANT var;
    VariantInit(&var);
    hr = spLegacy->GetCompByName(_bstr_t(_T("Hello World")), &var);

    3.2 Here, the address of "var" is passed to GetCompByName().

    3.3 Via the debugger, observe what is actually contained inside "var".

    3.4 It will be the address of the IntPtr "variant" that was returned from MarshalManagedToNative().

    3.5 And if you observe what is actually contained inside the memory area pointed to by the contents of "var", you will see that it is actually the BSTR VARIANT that you want.

    3.6 Use the following code to see what I mean :

    VARIANT var;
    VariantInit(&var);
    hr = spLegacy->GetCompByName(_bstr_t(_T("Hello World")), &var);
    VARIANT* pVar = *((VARIANT**)(&var));
    if(pVar -> vt == VT_BSTR)
    {
    	OutputDebugString(_T("VARIANT in pVar is a BSTR\n"));
    }

    4. I'm not sure what is the exact way to describe the error that produces the above observation or what can be done about it the way Session.GetCompByName() returns the VARIANT.

    5. I'll see what I can do. Will keep you posted.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Thursday, March 29, 2012 7:56 AM
  • Hello Brian,

    1. One possible solution is outlined below.

    2. Do not reference the interop assembly that originally defined the ILegacyInterface interface.

    3. Incorporate the following into your C# code :

    • Manually define the COM VARIANT structure in managed code :
    [StructLayout(LayoutKind.Sequential)]
    public struct Variant
    {
        public ushort vt;
        public ushort wReserved1;
        public ushort wReserved2;
        public ushort wReserved3;
        public IntPtr pVariantData;
        public IntPtr UnusedData;
    }
    • Manually define the ILegacyInterface interface in managed code :
    [ComImport()]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    [TypeLibType(4288)]
    public interface ILegacyInterface
    {
        [DispId(1)]
        Variant GetCompByName([In] string componentName);
    }

    4. Based on the above, the Session class and its GetCompByName() method can be coded as follows :

    [ComVisible(true)]
    [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("NetServer.Session")]
    public class Session : ILegacyInterface
    {
        [DispId(1)]
        public Variant GetCompByName([In] string componentName)
        {
            Variant components = new Variant();
            components.vt = 8;
            components.pVariantData = Marshal.StringToBSTR("Good luck Brian");
            return components;
        }
    }

    5. I have tried the above approach and it has worked in the case where the returned VARIANT contained a BSTR.

    6. However, your greater concern is to return a VARIANT that contains a SAFEARRAY of IDispatch interface pointers.

    7. Let me give this a try. Will keep you posted.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Thursday, March 29, 2012 9:01 AM
  • OK, so that helps explain why I couldn't get a simple variant across to the client. 

    A. Is there a way to dereference the VARIANT pointer inside of the customer marshaler?

    B. Since the standard marshaler can do a proper marshaling job wihtout my custom marshaler (as long as I DispatchWrap the array elements) can I create and invoke the standard marshaler from my custom marshaler?

    C. Creating a different interface definition for .NET doesn't seem like its going to solve my problem because .NET clients would then be getting back a VARIANT structure and not the array.  However if there was a way have interop go to one interface and .NET clients to another with no changes to either client, then that would be just as good of a solution.

    I appreciate the extra help you're providing!


    Brian R.

    Thursday, March 29, 2012 6:38 PM
  • Hello Brian,


    1. Concerning the Questions in the Last Post.

    1.1 I will leave aside the questions in your last post and address it later.

    1.2 For now, let me present a possible solution to the issue of returning a VARIANT that contains a SAFEARRAY of IDispatch interface pointers.

    1.3 The sections that follow will demonstrate such a sample solution presented step by step.

     

    2. Define the ILegacyInterface Interface and the VARIANT structure in a Separate Class Library.

    2.1 Create a separate class library and provide the definitions of the ILegacyInterface interface and the managed Variant structure.

    2.2 We also need to sign it with a strong name key file so that it can be referenced by the NetServer class library. I have also registered it to the GAC.

    2.3 The following is the full source :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace LegacyInterfacesManagedDefinitions
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct Variant
        {
            public ushort vt;
            public ushort wReserved1;
            public ushort wReserved2;
            public ushort wReserved3;
            public IntPtr pVariantData;
            public IntPtr UnusedData;
        }
    
        [ComImport()]
        [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
        [TypeLibType(4288)]
        public interface ILegacyInterface
        {
            [DispId(1)]
            Variant GetCompByName([In] string componentName);
        }
    }

    2.4 Remember to sign it with a strong name key file.


    3. Full Source of the NetServer.Session Class.

    3.1 The following is a full source of my sample NetServer.Session class :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.InteropServices;
    using LegacyInterfacesManagedDefinitions;
    
    namespace NetServer
    {
        [ComVisible(true)]
        [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")]
        [ClassInterface(ClassInterfaceType.None)]
        [ProgId("NetServer.Session")]
        public class Session : ILegacyInterface
        {
            [StructLayout(LayoutKind.Sequential)]
            struct SAFEARRAYBOUND
            {
                public UInt32 cElements;
                public Int32 lLbound;
            };
    
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern IntPtr SafeArrayCreateEx(VarEnum vt, uint cDims, SAFEARRAYBOUND[] rgsabound, IntPtr pvExtra);
    
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayPutElement(IntPtr pSafeArray, UInt32[] rgIndices, IntPtr pv);
    
            [DispId(1)]
            public Variant GetCompByName([In] string componentName)
            {
                Guid guid = typeof(string).GUID;
                GCHandle gch = GCHandle.Alloc(guid, GCHandleType.Pinned);
                IntPtr pGuid = gch.AddrOfPinnedObject();
    
                SAFEARRAYBOUND[] rgsabound = new SAFEARRAYBOUND[1];
                rgsabound[0].cElements = 4;
                rgsabound[0].lLbound = 0;
    
                IntPtr pSafeArray = SafeArrayCreateEx(VarEnum.VT_DISPATCH, 1, rgsabound, pGuid);
    
                gch.Free();
    
                for (int i = 0; i < stringArray.Length; i++)
                {
                    UInt32[] rgIndices = new UInt32[1];
                    // Indexing should be conformant to COM standards.        
    	            rgIndices[0] = (UInt32)i;
    
    	            SafeArrayPutElement
    	            (
                        pSafeArray,
    		            rgIndices,
                        Marshal.GetIDispatchForObject(stringArray[i])
    	            );
                }
                          
                Variant components = new Variant();
    
                components.vt = (ushort)(VarEnum.VT_ARRAY | VarEnum.VT_DISPATCH); //0x2009;
                components.pVariantData = pSafeArray;
    
                return components;
            }
    
            string[] stringArray = new string[4] { "John", "Paul", "George", "Ringo" };
        }
    }

    3.2 The following are some of the pertinent points about the code above :

    • It references the LegacyInterfacesManagedDefinitions class library which contained the definitions of the ILegacyInterface interface and the manager Variant structure.
    • It uses the SafeArrayCreateEx() and the SafeArrayPutElement() APIs exported from Oleaut32.dll to directly manage a SAFEARRAY from C#.
    • For demonstration purposes, I have decided to insert the IDispatch interfaces of 4 managed string instances into the SAFEARRAY to be returned to the caller.
    • The GUID of the managed string class is obtained via "Guid guid = typeof(string).GUID;". It is used in the call to SafeArrayCreateEx() in order to prepare a SAFEARRAY that will be used to contain IDispatch interface pointers.
    • The 4 string instances are members of the Session class : "string[] stringArray = new string[4] { "John", "Paul", "George", "Ringo" };".
    • Marshal.GetIDispatchForObject() is used to obtain a IDispatch interface pointer from a string instance which is expressed as an IntPtr type.
    • A managed Variant structure (defined inside the LegacyInterfacesManagedDefinitions class library) is instantiated, filled with values for its fields and then returned.

    3.3 The principle behind this implementation of GetCompByName() is to directly manipulate a SAFEARRAY.

    3.4 The following is my rough guess at what possibly happens under the covers at runtime :

    • The interop marshaler will allocate in memory (via Marshal.AllocCoTaskMem()) an unmanaged equivalent of the managed Variant structure. The amount of memory to be allocated is based on the definition of the managed Variant structure. As you no doubt are aware, it is 16 bytes.
    • Now, based on the way we have defined the managed Variant structure, its unmanaged equivalent will be the exact COM VARIANT structure itself.
    • The interop marshaler will blit the field values of the managed Variant structure "components" into this unmanaged COM VARIANT which has just been allocated in memory.
    • Now this unmanaged COM VARIANT will be returned to COM standard marshaler which will copy its contents (via VariantCopy()) to the VARIANT structure that the client code will already have provided.
    • After the VariantCopy() has been performed, the COM standard marshaler will free the unmanaged COM VARIANT that was allocated by the interop marshaler.

    4. Client Code.

    4.1 I shall post again to provide a C++ client code.

    4.2 After that, I will post again to provide a C# client code.


    - Bio.



    Please visit my blog : http://limbioliong.wordpress.com/

    Sunday, April 1, 2012 8:39 AM
  • Hello Brian,

    1. Presented below is the full source codes of a C++ client console application that is based on the original C++ client code listed in the OP :

    // CPPConsoleClient01.cpp : Defines the entry point for the console application.
    //
    #include "stdafx.h"
    #include <crtdbg.h>
    // {296AFBFF-1B0B-3FF5-9D6C-4E7E599F8B57}
    static const GUID CLSID_ManagedString = 
    { 0x296AFBFF, 0x1B0B, 0x3FF5, { 0x9D, 0x6C, 0x4E, 0x7E, 0x59, 0x9F, 0x8B, 0x57 } };
    void DispatchDisplayString(IDispatchPtr& spIDispatch)
    {
      // First obtain the dispatch id of the 
      // ToString() method.
      OLECHAR FAR* rgszNames = L"ToString";
      DISPID dispid = 0;
      spIDispatch -> GetIDsOfNames
      ( 
        IID_NULL,                  
        &rgszNames,  
        1,          
        LOCALE_SYSTEM_DEFAULT,                   
        &dispid          
      );
      // Next setup the call to ToString().
      DISPPARAMS dp;
      memset(&dp, 0, sizeof(DISPPARAMS));
      dp.cArgs = 0;
      // vr is the VARIANT that will obtain the 
      // return value of the call to ToString().
      VARIANTARG vr;
      VariantInit(&vr);
      
      UINT nErrArg;
      EXCEPINFO excepinfo;
      
      // Make the call.
      HRESULT hr = spIDispatch -> Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dp, &vr, &excepinfo, &nErrArg);
      
      // If the return value is indeed a BSTR,
      // display it on the console.
      if (V_VT(&vr) == VT_BSTR)
      {
    	printf ("Managed String : [%S].\r\n", V_BSTR(&vr));
      }
      // Clear the returned VARIANT vr.  
      VariantClear(&vr);
          
      return;
    }
    void TestSafeArrayOfDispatch(VARIANT& var)
    {
    	// Confirm that the contents of "var"
    	// is an array of IDispatch interface pointers.
    	if (V_VT(&var) != (VT_ARRAY | VT_DISPATCH))
    	{
    		return;
    	}
    	// Extract the SAFEARRAY from "var".  
    	SAFEARRAY*	pSafeArray = V_ARRAY(&var);
    	GUID		guid;
    	
    	// Get the GUID associated with the items 
    	// of the SAFEARRAY. This would be the 
    	// GUID of the managed string.
    	SafeArrayGetIID
    	(
    		pSafeArray,
    		&guid
    	);
    	// Confirm the GUID value.	
    	_ASSERT(guid == CLSID_ManagedString);
    	
    	long lLBound = 0;
    	long lUBound = 0;
    	
    	SafeArrayGetLBound
    	(
    		pSafeArray,
    		1,
    		&lLBound
    	);
      
    	SafeArrayGetUBound
    	(
    		pSafeArray,
    		1,
    		&lUBound
    	);
    	// Loop through the IDispatch interface 
    	// pointers of the managed strings 
    	// inside the SAFEARRAY and call each 
    	// string's ToString() method.
    	for (long l = lLBound; l <= lUBound; l++)
    	{
    		IDispatchPtr	spIDispatch = NULL;
    		long			ix = l;
    		
    		SafeArrayGetElement
    		(
    			pSafeArray,
    			&ix,
    			&spIDispatch
    		);
    		
    		DispatchDisplayString(spIDispatch);
    	}
    }
    int _tmain(int argc, _TCHAR* argv[])
    {
      CoInitialize(NULL);
      
      {
        ILegacyInterfacePtr spLegacy;
        HRESULT hr = spLegacy.CreateInstance(_T("NetServer.Session"));
        if(spLegacy)
        {
    		VARIANT var;
    		VariantInit(&var);
    		
    		hr = spLegacy->GetCompByName(_bstr_t(_T("Hello World")), &var);
    			
    		TestSafeArrayOfDispatch(var);
    	
    		TCHAR szBuf[200];
    		ZeroMemory(szBuf, sizeof(TCHAR)*200);
    		wsprintf(szBuf, _T("Called GetCompByName: hr=0x%08X"), hr); 
    		OutputDebugString(szBuf);
    		if(var.vt==VT_BSTR)
    		{
    			OutputDebugString(_T("VARIANT is a BSTR\n"));
    		}
    		// This call to VariantClear() is very important.
    		// It will perform a Release() to all the IDispatch
    		// interface pointers of the COM-Callable Wrappers
    		// of each of the managed string contained in the
    		// SAFEARRAY.
    		VariantClear(&var);
        }
      }
      
      CoUninitialize();
      return 0;
    }

    2. The following are significant points concerning the code above :

    • After the call to spLegacy->GetCompByName(), the TestSafeArrayOfDispatch() helper function is called to do some tests on the returned VARIANT "var".
    • TestSafeArrayOfDispatch() will do a rudimentary test to ensure that the incoming VARIANT "var" contains a SAFEARRAY of IDispatch interface pointers.
    • SafeArrayGetIID() is used to check that the GUID associated with the SAFEARRAY is that of the CCW of a managed string.
    • We then loop through the items in the SAFEARRAY and call DispatchDisplayString() for each IDispatch interface pointer.
    • DispatchDisplayString() will use IDispatch interface methods to call the ToString() method of each managed string in order to obtain the string value.
    • Each string value is then displayed on the console.

    3. The code presented in this post shows that the SAFEARRAY that was created and managed from the NetServer C# code is indeed viable for your purposes.

    4. In the next post, I shall present a C# client application.

    - Bio.

     


    Please visit my blog : http://limbioliong.wordpress.com/

    Monday, April 2, 2012 7:46 AM
  • Hello Brian,

    1. Presented below (in point 3) is the full source codes of a C# client console application that instantiates the COM object with progID "NetServer.Session" and calls its GetCompByName() method.

    2. Note that there are 2 separate techniques for displaying the contents of the returned VARIANT "var" :

    2.1 By using SAFEARRAY APIs.

    2.2 By transforming the returned VARIANT into a managed object.

    3. The code is listed below :

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Runtime.InteropServices;
    using LegacyInterfacesManagedDefinitions;
    namespace CSConsoleClient01
    {
        class Program
        {
            [DllImport("oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
            static extern Int32 VariantClear(IntPtr pvarg);
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetElement(IntPtr pSafeArray, UInt32[] rgIndices, out IntPtr pv);
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetLBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plLbound);
            [DllImport("Oleaut32.dll", CallingConvention = CallingConvention.StdCall)]
            private static extern uint SafeArrayGetUBound(IntPtr pSafeArray, UInt32 nDim, out Int32 plUbound);
            static void DoTest()
            {
                ILegacyInterface pILegacyInterface = (ILegacyInterface)Activator.CreateInstance
                    (Type.GetTypeFromProgID("NetServer.Session"));
                Variant var = pILegacyInterface.GetCompByName("ABC");
                if (var.vt == (ushort)(VarEnum.VT_ARRAY | VarEnum.VT_DISPATCH))
                {
                    IntPtr pSafeArray = var.pVariantData;
                    Int32 iLBound = 0;
                    Int32 iUBound = 0;
                    SafeArrayGetLBound(pSafeArray, 1, out iLBound);
                    SafeArrayGetUBound(pSafeArray, 1, out iUBound);
                    for (Int32 i = iLBound; i <= iUBound; i++)
                    {
                        UInt32[] rgIndices = new UInt32[1];
                        // Indexing should be conformant to COM standards.        
                        rgIndices[0] = (UInt32)i;
                        IntPtr pDispatch = IntPtr.Zero;
                        SafeArrayGetElement(pSafeArray, rgIndices, out pDispatch);
                        object obj = Marshal.GetObjectForIUnknown(pDispatch);
                        if (obj.GetType() == typeof(string))
                        {
                            Console.WriteLine("string[{0:D}] : {1:S}", i, (string)obj);
                        }
                    }
                }
                // When we have finished using the SAFEARRAY, we must
                // free it via VariantClear().
                // The call to VariantClear() is done by obtaining
                // a pointer to the VARIANT via the services of the
                // GCHandle class.
                GCHandle gch = GCHandle.Alloc(var, GCHandleType.Pinned);
                IntPtr pVariant = gch.AddrOfPinnedObject();
                VariantClear(pVariant);
                gch.Free();
            }
            static void DoTest2()
            {
                // Create an instance of the COM object with prog id
                // "NetServer.Session" and reference its ILegacyInterface
                // interface.
                ILegacyInterface pILegacyInterface = (ILegacyInterface)Activator.CreateInstance
                    (Type.GetTypeFromProgID("NetServer.Session"));
                // Call the interface's GetCompByName() method.
                Variant var = pILegacyInterface.GetCompByName("ABC");
                // Use the servces of the GCHandle class to obtain
                // a pointer to the returned VARIANT "var".
                GCHandle gch = GCHandle.Alloc(var, GCHandleType.Pinned);
                IntPtr pVariant = gch.AddrOfPinnedObject();
                // Use the Marshal.GetObjectForNativeVariant() method
                // to obtain a managed object that can be transformed
                // from "var".
                object objRet = Marshal.GetObjectForNativeVariant(pVariant);
                // Check to ensure that the object transformed from "var"
                // is a managed array of managed strings.
                if (objRet.GetType() == typeof(object[]))
                {
                    for (int i = 0; i < ((object[])objRet).Length; i++)
                    {
                        object an_object = ((object[])objRet)[i];
                        // If "an_object" is a string, display its value.
                        if (an_object.GetType() == typeof(string))
                        {
                            Console.WriteLine("string[{0:D}] : {1:S}", i, (string)an_object);
                        }
                    }
                }
                // The call to VariantClear() is very important because
                // the returned Variant "var" contains a SAFEARRAY even 
                // though we previously transformed it into a managed array.
                VariantClear(pVariant);
                gch.Free();
            }
            static void Main(string[] args)
            {
                DoTest();
            }
        }
    }

    The following are important points concerning the code above :

    • Note that the code references the LegacyInterfacesManagedDefinitions class library.
    • DoTest() will demonstrate how to manipulate the contents of the returned Variant "var" by using SAFEARRAY APIs.
    • DoTest2() will demonstrate how to manipulate the contents of the returned Variant "var" by first transforming it into a managed object.

    4. The following describes the DoTest() method :

    • DoTest() creates an instance of the COM object with progID "NetServer.Session".
    • The GetCompByName() method is then called. 
    • After checking to ensure that "var" is of VarType "VarEnum.VT_ARRAY | VarEnum.VT_DISPATCH", SAFEARRAY APIs are used to obtain the lower and upper bounds of the SAFEARRAY contained inside "var". 
    • DoTest() then loops through the SAFEARRAY and calls SafeArrayGetElement() to obtain the contents of the SAFEARRAY.
    • Marshal.GetObjectForIUnknown() is called to transform each IDispatch interface pointer into a managed object.
    • Once each managed object is confirmed to be a string, the string value is displayed.
    • Then at the end of DoTest(), the SAFEARRAY is freed using VariantClear().
    • The call to VariantClear() is done by obtaining a pointer to the VARIANT via the services of the GCHandle class.

    5. The following describes the DoTest2() method :

    • DoTest2() creates an instance of the COM object with progID "NetServer.Session".
    • The GetCompByName() method is then called.
    • The GCHandle class is then used to affix the returned Variant "var" in memory in order to obtain its address in memory.
    • Marshal.GetObjectForNativeVariant() is called to obtain a managed object that can be transformed entirely from "var".
    • After checking to ensure that the object transformed from "var" is a managed array of managed strings, each string value is displayed on the console through a "for" loop.
    • VariantClear() must still be called on "var" because it contains a SAFEARRAY after all.

    6. I will return to try to address the questions raised in one of your posts.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Monday, April 2, 2012 8:45 AM
  • Hello Brian,

    1. >> A. Is there a way to dereference the VARIANT pointer inside of the customer marshaler?

    1.1 I do not think this is possible.

    1.2 After all, the return value of the ICustomMarshaler.MarshalManagedToNative() method is an IntPtr.

    1.3 Under normal circumstances, the interop marshaler is called upon to perform the transformation of managed objects to unmanaged counterparts based on information specified in the interop assembly (in the case of COM interop) or the DllImportAttribute decorated external API declarations (in the case of PInvoke).

    1.4 The COM signature for the GetCompByName() method is :

    HRESULT GetCompByName([in] BSTR componentName, [out, retval] VARIANT *components);

    and the default managed signature for GetCompByName() is :

    object GetCompByName(string componentName);

    In this situation, the job of the interop marshaler is to transform the managed return value of type "object" into the contents of a COM VARIANT.

    1.5 Once a custom marshaler is specified, this transformation is not performed and whatever is returned from the ICustomMarshaler.MarshalManagedToNative() method of the custom marshaler is assumed to be fit for storing as the contents of the COM VARIANT.

    2. >> B. Since the standard marshaler can do a proper marshaling job wihtout my custom marshaler (as long as I DispatchWrap the array elements) can I create and invoke the standard marshaler from my custom marshaler?

    2.1 By "standard marshaler" I assume that you mean the COM standard automation marshaler.

    2.2 No, I do not see how you can "invoke" the COM standard marshaler in any way. The COM standard marshaler is used under the covers implicitly whenever COM marshaling is performed in unmanaged code. This happens only when cross-apartment marshaling is performed.

     

    3. >> C. Creating a different interface definition for .NET doesn't seem like its going to solve my problem because .NET clients would then be getting back a VARIANT structure and not the array...

    3.1 The idea is to use SAFERRAY APIs to manipulate the SAFEARRAY contained in the returned Variant structure (which is logically equivalent to the COM VARIANT).

    3.2 As shown in the sample C# client code, this is possible.

     

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Monday, April 2, 2012 9:37 AM
  • I appreciate the enthusiasm you've taken in this problem and you've presented a plethora of ideas, but the issue that cannot be overcome thus far is that I can't change the client code.  We are adapting/migrating a legacy application that has many companies components that integrate with it.  This is a public interface that has to be supported. 

    In my application the client's call GetCompByName do the following:

    C++ Example:

       _variant_t comp;
       comp =  m_session_mgr->GetCompByName(_bstr_t(_T("ComponentProgID.ProgIDExample")));
       
       if ((VT_ARRAY | VT_DISPATCH) == comp.vt)
       {
          // Extract the array pointer.
          SAFEARRAY* const sa = comp.parray;
          
          // Get the first element in the array
          SAFEARRAYBOUND bound = sa->rgsabound[0];
          long count = bound.cElements;
          
          // there had beter be only one of these in the system....
          if (1 != count)
          {
             return NULL;
          }
          
          // get the first (and hopefully only) interface
          IDispatchPtr dispatch = NULL;
          SafeArrayGetElement(sa, &count, &dispatch);
        
       }

    C# Example:

              session = value;
              Array var = null;

              var = (Array)session.GetCompByName("ProgId Example");
              myObject = (IMyInterface)var.GetValue(var.GetLowerBound(0));

    As you can see from the examples, defining a managed interface that returns a VARIANT woudl work for  the C++ and VB6 clients, but would break my .NET clients.  This is why I went down the custom marshaler path because it would only come into play when being invoked from C++.

    Summary

    So with a custom marshaler I can get a SAFEARRAY across the wire as a VARIANT**, but not a VARIANT*.  The .NET standard marshaler for .NET Interop can do this if I return an array of dispatch wrapper objects from .NET.  However, I can't invoke the .NET marshal code/component/capability from my custom marshaler -- (not the COM marshaler).  Lastly, defining an interface in .NET that returns a VARIANT will help my C++ clients, but will break my .NET clients.

    Any other ideas would be helpful.

    Thanks,


    Brian R.

    Tuesday, April 3, 2012 5:26 PM
  • An interface shouldn't break your .net clients.


    Ghost,
    Call me ghost for short, Thanks
    To get the better anwser, it should be a better question.

    Thursday, April 12, 2012 7:05 AM
  • After submitting this concern to Microsoft for a technical support issue they've noted that a CustomMarshaler cannot be used because it does not support value type marshaling.  Because of the signature of GetCompByName, which passes a VARIANT, it can't be done this way.

    Also, using a different interface definition is not possible because .NET and COM alike cast the .NET server to the same interface. 

    Thanks to all that tried to help.


    Brian R.

    • Marked as answer by Brian R. _ Thursday, April 26, 2012 6:39 PM
    Thursday, April 26, 2012 6:39 PM