How To Interoperate with a COM Server That Returns Conformant Arrays by Using Visual C# .NET (305990)



The information in this article applies to:

  • Microsoft Visual C# .NET (2002)

This article was previously published under Q305990
For a Microsoft Visual Basic .NET version of this article, see 306801.

This article refers to the following Microsoft .NET Framework Class Library namespace:
  • System.Runtime.InteropServices

IN THIS TASK

SUMMARY

This step-by-step article describes how to interoperate with a COM server that returns conformant arrays.

The following file is available for download from the Microsoft Download Center:
For additional information about how to download Microsoft Support files, click the following article number to view the article in the Microsoft Knowledge Base:

119591 How to Obtain Microsoft Support Files from Online Services

Microsoft scanned this file for viruses. Microsoft used the most current virus-detection software that was available on the date that the file was posted. The file is stored on security-enhanced servers that help to prevent any unauthorized changes to the file. back to the top

The Sample COM Server

The server that is used in this example implements the following interface:
interface IComArrsObj : IDispatch
{
   HRESULT GetArrOfLongs([in] LONG nStartIdx, [in,out] LONG* pnCnt,
                         [out,size_is(,*pnCnt)] LONG** ppArr);
   HRESULT GetArrOfUDTs ([in] LONG nStartIdx, [in,out] LONG* pnCnt,
                         [out,size_is(,*pnCnt)] MyUDT** ppArr);
   HRESULT MyNextLongs  ([in] LONG nReq, [out, size_is(nReq)] LONG *rgelt,
                         [out] LONG* pnFetched);
   HRESULT MyNextUDTs   ([in] LONG nReq, [out, size_is(nReq)] MyUDT* rgelt,
                         [out] LONG* pnFetched);
};
				
As shown in this interface specification, this article demonstrates how to interoperate with a COM server that returns an array of Long values and an array of a user-defined type (UDT). The code also illustrates the difference between server allocated arrays (that is, the GetArrOfXxx methods) and client allocated arrays (that is, the MyNextXxx methods that mimic the Next method idiom from the IEnumXxx interfaces).

The GetArrOfXxx methods take the following three arguments:
  • nStartIdx is an [in] argument that specifies the index of the first element to pass back to the client.
  • pnCnt is an [in, out] argument that equals the number of items that are requested by the client upon entry, and the number of items that are provided by the server upon exit. If pnCnt is zero upon entry, the server returns all available elements.
  • ppArr is an [out] array that is allocated by the server. Its size equals *pnCnt.
The MyNextXxx methods take the following three arguments:
  • nReq is an [in] argument that specifies the number of items that are requested by the client.
  • rgelt is an [out] argument that is allocated by the client and filled by the server. Only the first *pnFetched elements are valid.
  • pnFetched is an [out] argument that specifies the number of items that are filled in by the server.
back to the top

Discussion of Approaches to COM Interop

Generally, when you interoperate with a COM server, you have the following options:
  • Use an interop assembly to communicate with the COM server. If one is available, you should use a primary interop assembly. Otherwise, you can generate the interop assembly by using the Tlbimp.exe utility or the import functionality of Visual Studio .NET.
  • Declare the COM objects and interfaces directly in the Visual C# .NET source code. You can use this option if you need to interoperate with a limited number of objects and interfaces that are exposed by the COM server.
  • You may enhance either of these options by adding a managed class that provides a familiar managed interface for the rough import classes that are needed to interoperate with the COM server.
You should always use a primary interop assembly if one is available. When the primary interop assembly is available, you may still use the third option and provide a managed interface for the types that are exported from the primary interop assembly.

When a primary interop assembly is not available, the option that you use depends on whether the application needs to expose types that are imported from the COM server. This may occur when the application consists of multiple assemblies that exchange data of a type that is imported from the COM server.

If the managed application contains multiple assemblies that use the COM server and that need to expose a data type that is imported from the COM server, you should use a shared assembly to define the interop assembly. In such a situation, using a primary interop assembly is strongly recommended.

If you do not need to expose data types from the COM server across assemblies, you can use either private assemblies for the interop assembly (the first option) or define the COM types in Visual C# .NET code (the second option).

For more information, see the following Microsoft Developer Network (MSDN) Web site: back to the top

Use Interop Assemblies

You should always use a primary interop assembly if one is available. Use of a primary interop assembly provides for a well-tested, proven interop layer to access the COM server functionality. In addition, Tlbimp.exe provides an automatic way to generate the interop assembly, resulting in advantages of automatic code generation such as speed and correctness. Also, when the managed client needs to access a large number of the methods and interfaces that are exposed by the COM server, the second option may prove prohibitive. In these situations, you may use an interop assembly to access the functionality that the COM server provides. You can obtain the interop assembly by using one of the following methods:
  • Obtain the primary interop assembly from the software vendor that publishes the COM server with which you need to interoperate.
  • Generate a primary interop assembly from Visual Studio .NET. To invoke Tlbimp.exe to generate the interop assembly that you need, click Add Reference on the Project menu, click the COM tab, and then select the entry for the COM server.
  • Generate the interop assembly manually. To do this, invoke Tlbimp.exe on the COM server type library (that is, on the .tlb, .dll, .ocx, or .exe file). This allows you to have finer control over the behavior of Tlbimp.exe, rather than relying on the Visual Studio .NET default command line arguments.
  • For the highest degree of flexibility, follow these steps:
    1. Use Tlbimp.exe to generate a first version of the interop assembly, then run Ildasm.exe on the assembly. To do this, type ildasm Interop.Srvr.dll /out=Interop.Srvr.il at a command prompt.
    2. Hand-edit the generated intermediate language code to specify the marshalling behavior that you need.
    3. Reassemble the intermediate language code by using Ilasm.exe to obtain the final interop assembly that you reference in your project. To do this, type ilasm /dll Interop.Srvr.il /out=Interop.Srvr.dll at a command prompt.
Because of COM type library format limitations, Tlbimp.exe cannot generate correct interface definitions for the COM server in the sample that this article provides. For this reason, the sample in this article uses the fourth option. To use this option, follow these steps:
  1. Run Tlbimp.exe on the COM server dynamic-link library (DLL), as follows:
       > tlbimp /out:Interop_1.ComArrs_1_0.DLL /namespace:ComArrs_1 ComArrs.DLL 
    					
  2. Run Ildasm.exe on the generated DLL, as follows:
       > ildasm /out=Interop_1.ComArrs_1_0.IL Interop_1.ComArrs_1_0.DLL
    					
  3. Modify the method signatures in the generated intermediate language file to modify the marshaling according to your needs. Each method appears twice in the intermediate language, so you must make these changes in both the declaration of the interface and the declaration of the class:
       .method public hidebysig newslot virtual abstract 
               instance void  GetArrOfLongs([in] int32 nStartIdx,
                                            [in][out] int32& pnCnt,
                                            // Replace this: [out] native int ppArr) runtime managed internalcall
                                            // with this:
                                            [out] native int& ppArr) runtime managed internalcall
    					
       .method public hidebysig newslot virtual abstract 
               instance void  GetArrOfUDTs([in] int32 nStartIdx,
                                           [in][out] int32& pnCnt,
                                           // Replace this: [out] valuetype ComArrs_1.MyUDT&  marshal( lpstruct) ppArr) runtime managed internalcall
                                           // With this:
                                           [out] native int& ppArr) runtime managed internalcall
    					
       .method public hidebysig newslot virtual abstract 
               instance void  MyNextLongs([in] int32 nReq,
                                     // Replace this: [out] int32& rgelt,
                                     // With this:
                                     [out] int32[] marshal([ + 1]) rgelt,
                                     [out] int32& pnFetched) runtime managed internalcall
    					
       .method public hidebysig newslot virtual abstract 
               instance void  MyNextUDTs([in] int32 nReq,
                                         // Replace this: [out] valuetype ComArrs_1.MyUDT& rgelt,
                                         // With this:
                                         [out] valuetype ComArrs_1.MyUDT[] marshal([ + 1]) rgelt,
                                         [out] int32& pnFetched) runtime managed internalcall
    					
  4. Use Ilasm.exe to rebuild the interop assembly, as follows:
       > ilasm /dll /resource=Interop_1.ComArrs_1_0.res Interop_1.ComArrs_1_0.il /out=Interop_1.ComArrs_1_0.dll
    					
  5. Reference the new interop assembly in the Visual C# .NET client project. To do this, click Add Reference on the Project menu, then browse to the newly-generated DLL.
  6. Write the test code in the managed client, as follows:
       public static void Run ( )
       {
          Console.WriteLine ( "\nTesting hand-edited IL of TlbImp-ed COM class:" );
          Console.WriteLine (   "=============================================:" );
    
          Console.WriteLine ( "Creating COM object" );
          ComArrs_1.CComArrsObj o = new ComArrs_1.CComArrsObj ( );
    
          Console.WriteLine ( "Calling GetArrOfLongs( )" );
          int cnt = 0;
          IntPtr rAddr;
          o.GetArrOfLongs ( 0, ref cnt, out rAddr );
          int [ ] r = new int [ cnt ];
          // Marshal the array from an unmanaged to a managed heap.
          Marshal.Copy ( rAddr, r, 0, cnt );
          // Release the unmanaged array.
          Marshal.FreeCoTaskMem ( rAddr );
          Utils.PrintArray ( r, elementFormatter );
    
          Console.WriteLine ( "Calling GetArrOfUDTs( )" );
          cnt = 0;
          IntPtr ruAddr;
          o.GetArrOfUDTs ( 0, ref cnt, out ruAddr );
          ComArrs_1.MyUDT [ ] ru = new ComArrs_1.MyUDT [ cnt ];
          // Marshal the array, element by element, from an unmanaged to a managed heap.
          for (int i = 0, elemOffs = (int) ruAddr; i < cnt; i++ )
          {
              ru[i] = ( ComArrs_1.MyUDT ) Marshal.PtrToStructure (
                             (IntPtr) elemOffs, typeof ( ComArrs_1.MyUDT ) );
              elemOffs += Marshal.SizeOf ( typeof ( ComArrs_1.MyUDT ) );
          }
          // Release the unmanaged array.
          Marshal.FreeCoTaskMem ( ruAddr );
          Utils.PrintArray ( ru, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextLongs( )" );
          int [ ] q = new int [ 20 ];
          // The marshalling is performed automatically.
          o.MyNextLongs ( q.GetLength(0), q, out cnt );
          Utils.PrintArray ( q, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextUDTs( )" );
          ru = new ComArrs_1.MyUDT[20];
          // The marshalling is performed automatically.
          o.MyNextUDTs ( ru.GetLength(0), ru, out cnt );
          Utils.PrintArray ( ru, elementFormatter );
       }
    					
back to the top

Use C# Declarations for the COM Server

The advantage of this option over using an interop assembly is that it eliminates the need to ship one additional file (that is, the interop assembly).

To specify the necessary elements and call the methods, follow these steps:

NOTE: Start with the COM interface that is provided in the "Sample COM Server" section.
  1. Define the UDT as follows:
       // MyUDT as defined in the server.
       [Guid("190A418D-B113-40d4-A22C-20EF9EAC3E33")]
       [StructLayout(LayoutKind.Sequential)]
       struct MyUDT
       {
          [MarshalAs(UnmanagedType.BStr)]
          public string   aBstr;
          public int      aLong;
          public bool     aBool;
       }
    					
  2. Define the COM interface as follows:
       // A possible C# representation of the interface.
       [InterfaceType(ComInterfaceType.InterfaceIsDual),
          Guid("78D4F391-B10B-4B80-A2D1-1B4C583DCAEC")]
       interface IComArrsObj
       {
          void GetArrOfLongs(int startIdx, ref int cnt, out IntPtr arrAddr);
          void GetArrOfUDTs (int startIdx, ref int cnt, out IntPtr arrAddr);
          void MyNextLongs  (int req,
                             [MarshalAs(UnmanagedType.LPArray, Out]
                             int [ ] rgelt,
                             out int fetched);
          void MyNextUDTs   (int req,
                             [MarshalAs(UnmanagedType.LPArray, Out]
                             MyUDT [ ] rgelt,
                             out int fetched);
       }
    					
  3. Define the COM server as follows:
       // The coclass.
       [ComImport, Guid("056A32CF-D716-4902-BCD2-ED7F070D9E36")]
       class CComArrsObj
       {
       }
    					
  4. Call the methods on the COM server as follows:
       public static void Run ( )
       {
          Console.WriteLine ( "\nTesting C#-declared COM class and interfaces:" );
          Console.WriteLine (   "============================================:" );
    
          Console.WriteLine ( "Creating COM object" );
          ComArrs_0.IComArrsObj icao = new ComArrs_0.CComArrsObj ( )
                                           as ComArrs_0.IComArrsObj;
    
          Console.WriteLine ( "Calling GetArrOfLongs( )" );
          int cnt = 0;
          IntPtr rAddr;
          icao.GetArrOfLongs ( 0, ref cnt, out rAddr );
          int [ ] r = new int [ cnt ];
          // Marshal the array from an unmanaged to a managed heap.
          Marshal.Copy ( rAddr, r, 0, cnt );
          // Release the unmanaged array.
          Marshal.FreeCoTaskMem ( rAddr );
          Utils.PrintArray ( r, elementFormatter );
    
          Console.WriteLine ( "Calling GetArrOfUDTs( )" );
          cnt = 0;
          IntPtr ruAddr;
          icao.GetArrOfUDTs ( 0, ref cnt, out ruAddr );
          ComArrs_0.MyUDT [ ] ru = new ComArrs_0.MyUDT [ cnt ];
          // Marshal the array, element by element, from an unmanaged to a managed heap.
          for ( int i = 0, elemOffs = (int) ruAddr; i < cnt; i++ )
          {
             ru[i] = ( ComArrs_0.MyUDT ) Marshal.PtrToStructure (
                            (IntPtr) elemOffs, typeof ( ComArrs_0.MyUDT ) );
             elemOffs += Marshal.SizeOf ( typeof ( ComArrs_0.MyUDT ) );
          }
          // Release the unmanaged array.
          Marshal.FreeCoTaskMem ( ruAddr );
          Utils.PrintArray ( ru, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextLongs( )" );
          int [ ] q = new int [20];
          // The marshalling is performed automatically.
          icao.MyNextLongs ( q.GetLength(0), q, out cnt );
          Utils.PrintArray ( q, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextUDTs( )" );
          ru = new ComArrs_0.MyUDT [20];
          // The marshalling is performed automatically.
          icao.MyNextUDTs ( ru.GetLength(0), ru, out cnt );
          Utils.PrintArray ( ru, elementFormatter );
       }
    					
back to the top

Add a Managed Wrapper Class

The managed wrapper class can be based on either of the previous two options. It can use the TlbImp-generated wrapper from the first option, or it can encapsulate a reference to the Visual C# .NET declarations of the COM server. To use the latter option for building the managed wrapper class, follow these steps:
  1. Add a reference to the interop assembly that contains the managed definition of the COM server.
  2. Define the managed wrapper class in terms of the class that TlbImp has generated, as follows:
       class MgdComArrs
       {
          public MgdComArrs ( )
          { cao_ = new ComArrs_1.CComArrsObj( ); }
    
          public int [ ] GetArrOfLongs ( int startIdx, ref int cnt )
          {
             IntPtr rAddr;
             cao_.GetArrOfLongs ( startIdx, ref cnt, out rAddr );
             int [ ] r = new int [ cnt ];
             // Marshal the array from an unmanaged to a managed heap.
             Marshal.Copy ( rAddr, r, 0, cnt );
             // Release the unmanaged array.
             Marshal.FreeCoTaskMem ( rAddr );
    
             return r;
          }
    
          public ComArrs_1.MyUDT [ ] GetArrOfUDTs ( int startIdx, ref int cnt )
          {
             IntPtr ruAddr;
             cao_.GetArrOfUDTs ( startIdx, ref cnt, out ruAddr );
             ComArrs_1.MyUDT [ ] ru = new ComArrs_1.MyUDT [ cnt ];
             // Marshal the array, element by element.
             for ( int i = 0, elemOffs = (int) ruAddr; i < cnt; i++ )
             {
                ru[i] = (ComArrs_1.MyUDT) Marshal.PtrToStructure (
                              (IntPtr) elemOffs, typeof ( ComArrs_1.MyUDT ) );
                elemOffs += Marshal.SizeOf ( typeof ( ComArrs_1.MyUDT ) )'
             }
             // Release the unmanaged array.
             Marshal.FreeCoTaskMem ( ruAddr );
    
             return ru;
          }
    
          public int MyNextLongs ( int req, int [ ] r )
          {
             int fetched;
             cao_.MyNextLongs ( req, r, out fetched );
             return fetched;
          }
    
          public int MyNextUDTs ( int req, ComArrs_1.MyUDT [ ] r )
          {
             int fetched;
             cao_.MyNextUDTs ( req, r, out fetched );
             return fetched;
          }
    
          private ComArrs_1.CComArrsObj cao_;
       }
    					
  3. Write the client code, as follows:
       public static void Run ( )
       {
          Console.WriteLine ( "\nTesting managed wrapper definition of COM class:" );
          Console.WriteLine (   "===============================================:" );
          Console.WriteLine ( "Creating managed wrapper" );
          MgdComArrs mcao = new MgdComArrs( );
    
          Console.WriteLine ( "Calling GetArrOfLongs( )" );
          int cnt = 0;
          int [ ] r = mcao.GetArrOfLongs ( 0, ref cnt );
          Utils.PrintArray ( r, elementFormatter );
    
          Console.WriteLine ( "Calling GetArrOfUDTs( )" );
          cnt = 0;
          ComArrs_1.MyUDT [ ] ru = mcao.GetArrOfUDTs ( 0, ref cnt );
          Utils.PrintArray ( ru, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextLongs( )" );
          int [ ] q = new int [ 15 ];
          mcao.MyNextLongs ( q.GetLength ( 0 ), q );
          Utils.PrintArray ( q, elementFormatter );
    
          Console.WriteLine ( "Calling MyNextUDTs( )" );
          ru = new ComArrs_1.MyUDT [ 5 ];
          mcao.MyNextUDTs ( ru.GetLength ( 0 ), ru );
          Utils.PrintArray ( ru, elementFormatter );
       }
    					
back to the top

REFERENCES

For more information, see the following Microsoft Developer Network (MSDN) Web sites: back to the top

Modification Type:MinorLast Reviewed:8/5/2004
Keywords:kbdownload kbCOMInterop kbHOWTOmaster kbMarshal kbSample KB305990 kbAudDeveloper