Advanced Interop Marshaling Considerations

Team-Fly    

 
.NET and COM Interoperability Handbook, The
By Alan Gordon
Table of Contents
Chapter Seven.  Advanced .NET to COM Interop

Advanced Interop Marshaling Considerations

When you use a COM object from a managed client or a managed object from a COM/Win32 client, the CLR must perform marshaling. For the purposes of this discussion, marshaling is just converting the parameters from managed types on the client to unmanaged types in the object and vice versa. What I am talking about here is different than COM marshaling. As I discussed earlier, if your managed thread has entered an MTA and you are calling a single-threaded (STA) object, cross-apartment marshaling will be used. Similarly, interprocess marshaling will be used if you use an object that is implemented in an out-of-process server. This marshaling is independent of the managed-to-unmanaged code marshaling that I am talking about here. What I am talking about here is the marshaling that occurs when I cross the managed code to unmanaged code boundary as shown in Figure 7-9.

Figure 7-9. Managed to unmanaged code marshaling.

graphics/07fig09.gif

For most objects with simple parameter lists and return values, the Interop Marshaler in the CLR handles this marshaling automatically. So far, all of the COM objects that I have examined have had very simple marshaling requirements. None of the methods or properties in these objects returned arrays or user-defined types or took arrays of user -defined types as parameters. In this section, I discuss how Interop marshaling works when your parameter lists and return values aren't so simple.

With most COM objects, a string is declared as a BSTR, but it may also be defined using one of several other string definitions, including a char *, an LPOLESTR, or a LPWSTR. In managed code, however, a string is always an instance of System.String. The Interop marshaler must be able to convert an instance of the managed type, System.String, to any of the unmanaged representations of a string. There are also times when the type library importer will effectively throw up its hands and decide that it cannot find a good managed type to map a parameter or return value of a COM object to. In these cases, you'll have to perform manual marshaling, that is, you will have to write the code to marshal parameters and return values from a managed type to an unmanaged type and vice versa. Arrays are another problem with COM Interop. Dealing with C-style arrays, which are usually expressed as a pointer to a type in unmanaged code, is especially difficult because it is hard for the Interop marshaler to tell if it is dealing with an array or a single instance. For instance, if I have a COM object whose IDL definition is as follows :

 [id(3)] HRESULT Conformant1D([in] short arrlen,       [in, size_is(arrlen)] short *arg); 

It is difficult to tell whether the arg parameter is a pointer to a short or an array or an array of shorts. The size_is attribute actually does specify that the arg parameter is an array whose size is given by the first parameter arrlen. However, this information is not present in a type library that I generate from this IDL code.

In the following subsections, I discuss the following aspects of interop marshaling: COM objects that use strings that are typed as something other than BSTR (for example, LPOLESTR) and arrays, including both safe arrays and c-style arrays. In the arrays section, I also discuss manual marshaling, that is, what to do when the interop marshaler throws up its hands and converts a parameter to a System.IntPtr in the Interop assembly.

String Parameters Other than BSTR

I decided to start this difficult section with the easiest subject to talk about: how the Interop Marshaler handles string parameters or return values that are expressed as something other than a BSTR. The transformation here is fairly simple. The type library importer will convert the parameter or return value to a System.String and use the MarshalAs attribute, which can be found in the System.Runtime.InteropServices namespace, to specify that the managed string should be converted to something other than a BSTR on the unmanaged side of the Interop boundary. Table 6-1 in Chapter 6 showed the default mapping of COM types to managed types. By default, the unman aged equivalent of a System.String is a BSTR, but System.String can map to other unmanaged representations of a string. The Type Library Importer will use the MarshalAs attribute to specify which mapping should be used when a managed type can map to several unmanaged types as System.String does.

This is best illustrated with an example. Consider the following COM interface, which uses ANSI strings:

 interface ISpellChecker : IUnknown {   HRESULT CheckSpelling([in,string] char *word,            [out,retval] BOOL *isCorrect);   HRESULT UseCustomDictionary([in,string] char *filename); } 

This interface will convert to the following managed code equivalent:

 public interface ISpellChecker {     public int32 CheckSpelling([in] string  MarshalAs(lpstr) word);     public void  UseCustomDictionary([in] string  MarshalAs(lpstr) filename) ; } 

Notice the use of the MarshalAs attribute to convert the string to an lpstr. Now consider the following COM Interface, which uses wide character strings:

 interface ISpellChecker : IUnknown {     HRESULT CheckSpelling([in] LPOLESTR word,         [out,retval] VARIANT_BOOL *result);     HRESULT UseCustomDictionary([in] LPOLESTR path); }; 

This interface will map to the following managed code equivalent:

 public interface ISpellChecker {     public bool CheckSpelling([in] string  MarshalAs(lpwstr) word);     public void  UseCustomDictionary([in] string  MarshalAs(lpwstr) filename) ; } 

In this case the Interop assembly uses the MarshalAs attribute to convert the managed string to an lpwstr.

Arrays

I can guarantee that you won't be using COM Interop for very long before you will need to use a COM object that uses arrays in its interface. There are some difficulties associated with using Arrays, particularly c-style arrays, but, as long as you understand the concepts in this section, you won't have a problem.

SAFE ARRAYS

Safe arrays are a self-describing COM type that can be used to pass multidimensional arrays through a COM Interface. Safe arrays contain enough information for the receiver of the array to know exactly how many dimensions and elements there are in the array as well as the type of each element. The definition of the SafeArray data type looks as follows:

 struct SAFEARRAY {       WORD cDims;// The number of dimensions       WORD fFeatures;   // A descriptive flag       DWORD cbElements;// The size of each element in bytes       DWORD cLocks;// A reference count for pvData       void * pvData;// The data of the array       [size_is(cDims)] SAFEARRAYBOUND rgsabound[]; } 

The rgsabounds array contains an array of SAFEARRAYBOUND structures, which are defined as follows:

 Struct SAFEARRAYBOUND {       DWORD cElements; // The # of elements in this dimension       LONG iLBound;   // The minimum index for this dimension } 

With C/C++, you can create and destroy safe arrays using the SafeArrayCreate and SafeArrayDestroy functions, respectively. You can access the individual elements in a SafeArray using the SafeArrayGetElement function. I won't go into a long discussion about how to use safe arrays. If you want to learn more about them, see the article entitled "The Safe OLE Way of Handling Arrays" in the Microsoft Developer Network (MSDN) libraries.

It's easy to create and use SafeArrays from VB. A dynamic array in VB is actually a SafeArray. To demonstrate safe arrays, I created a COM component that uses safe arrays in VB by creating an ActiveX DLL project and adding a class to the project with the following implementation:

 Public Type udtEmployee     ID As Integer     Name As String     Salary As Currency End Type Public Function TestSafeArrayOfStrings() As String() // // Implementation omitted... // End Function Public Function TestSafeArrayOfIntegers() As Integer() // // Implementation omitted... // End Function Public Function TestSafeArrayOfUDTs() As udtEmployee() // // Implementation omitted... // End Function Public Sub TestSafeArrayAsParameter1(strs() As String) // // Implementation omitted... // End Sub Public Sub TestSafeArrayAsParameter2(Employees() As udtEmployee) // //  Implementation omitted... End Sub 

The first two methods, TestSafeArrayOfStrings and TestSafeArrayOfIntegers, return a safe array of strings and integers, respectively. The TestSafeArrayOfUDTs method returns a safe array of User-Defined Types (UDTs), and the fourth and fifth methods, TestSafeArrayAsParameter1 and TestSafeArrayAsParameter2, demonstrate methods that take a safe array of strings and UDTs as parameters, respectively. The IDL for this COM component is the following:

 [   uuid(9101979F-2B8C-4EA5-9FFC-DC588A07C6A9),   version(5.0) ] library TestProject {     [       odl,       uuid(D861E9BC-B4ED-47F0-926F-C6A4900BB5D5),       version(1.0),       hidden,       dual,       nonextensible,       oleautomation     ]     interface _TestClass : IDispatch {         [id(0x60030000)]         HRESULT TestSafeArrayOfStrings([out, retval] SAFEARRAY(BSTR)*);         [id(0x60030001)]         HRESULT TestSafeArrayOfIntegers([out, retval] SAFEARRAY(short)*);         [id(0x60030002)]         HRESULT TestSafeArrayOfUDTs([out, retval] SAFEARRAY(udtEmployee)*);         [id(0x60030003)]         HRESULT TestSafeArrayAsParameter1([in, out] SAFEARRAY(BSTR)* strs);         [id(0x60030004)]         HRESULT TestSafeArrayAsParameter2([in, out] SAFEARRAY(udtEmployee)* Employees);     };     [       uuid(74A9A0C5-6589-4E41-9F0E-CF26DCDC0DC9),       version(1.0)     ]     coclass TestClass {         [default] interface _TestClass;     };     typedef [uuid(2063C9B0-4CC5-4271-BAE4-F56EC2EE0197),           version(1.0)]     struct tagudtEmployee {         [helpstring("ID")]         short ID;         [helpstring("Name")]         BSTR Name;         [helpstring("Salary")]         CURRENCY Salary;     } udtEmployee; }; 

I will use this COM component to demonstrate how the type library importer maps the SafeArrays in these methods to managed types. Before I show the mapping, I should mention that the tlbimp has two modes of operation, depending on whether you specify the sysarray parameter. If I run tlbimp on a COM server without using the sysarray argument, it will generate typed arrays in the Interop assembly for the SafeArray parameters and return values. If I use the sysarray parameter on tlbimp as follows:

 tlbimp testproject.dll /sysarray 

Then the Type Library Importer will convert the SafeArrays to instances of a class called System.Array . The System.Array class is the base class for all managed code arrays. For instance, the TestSafeArrayOfStrings method, which has the following definition in IDL:

 [id(0x60030000)] HRESULT TestSafeArrayOfStrings([out, retval] SAFEARRAY(BSTR)*); 

will generate the following managed equivalent in the Interop assembly if I do not use the /sysarray parameter to tlbimp:

 string[] [MarshalAs(safearray bstr)]     TestSafeArrayOfStrings() { } 

Notice that the return type is an array of System.Strings. You can call this code from a C# client using the following code:

 int i; string strName; TestClass myClass = new TestClass(); string[] arr=myClass.TestSafeArrayOfStrings(); for (i=0;i<arr.Length;i++)         strName=arr[i]; 

If I use the /sysarray parameter, tlbimp will generate the following managed code equivalent:

 System.Array [MarshalAs(safearray bstr)]     TestSafeArrayOfStrings() { } 

This time the method returns an instance of System.Array. I can call this version of the method from a client as follows:

 int i; string strName; TestClass myClass = new TestClass(); System.Array arr=myClass.TestSafeArrayOfStrings(); for (i=0;i<arr.Length;i++)  strName=(string)arr.GetValue(i);  

The client code is almost the same except that I have to cast the elements in the array to a string because the elements of System.Array are typed to System.Object. If you use Visual Studio .NET to generate the Interop assembly for a COM server by selecting the COM server from the COM tab of the Add Reference dialog in the IDE, it will run the Type Library Importer with the /sysarray parameter. Therefore, if you want the default behavior, you will need to run the tlbimp manually and then reference the resulting Interop assembly using the .NET tab of the Add Reference dialog.

Let's look at the other methods in the example SafeArray COM server. The TestSafeArrayOfIntegers method, which has the following IDL definition:

 [id(0x60030001)] HRESULT TestSafeArrayOfIntegers([out, retval] SAFEARRAY(short)*); 

will generate the following managed code equivalent if you do not specify /sysarray:

 int16[] [MarshalAs(safearray int16)]         TestSafeArrayOfIntegers() { } 

You can call this method from a managed code client using the following code:

 int i; short shtValue; TestClass myClass = new TestClass(); short[] arr=myClass.TestSafeArrayOfIntegers(); for (i=0;i<arr.Length;i++)         shtValue=arr[i]; 

If you use the /sysarray parameter, tlbimp will generate the following managed code equivalent for the TestSafeArrayOfIntegers method:

 System.Array [MarshalAs(safearray int16)]        TestSafeArrayOfIntegers() { } 

You can call this method from a C# client using the following code:

 int i; short shtValue; TestClass myClass = new TestClass(); System.Array arr=myClass.TestSafeArrayOfIntegers(); for (i=0;i<arr.Length;i++)          shtValue=(short)arr.GetValue(i); 

The TestSafeArrayOfUDTs is a little different than the first two methods because it returns a SafeArray of UDTs. In this case, the UDT is called udtEmployee. The IDL definition of this type is shown here:

 typedef [uuid(2063C9B0-4CC5-4271-BAE4-F56EC2EE0197)] struct tagudtEmployee {     [helpstring("ID")] short ID;     [helpstring("Name")] BSTR Name;     [helpstring("Salary")] CURRENCY Salary; } udtEmployee; 

The IDL for the TestSafeArrayOfUDTs method itself is shown here:

 [id(0x60030002)] HRESULT TestSafeArrayOfUDTs([out, retval] SAFEARRAY(udtEmployee)*); 

This method will generate the following managed code equivalent if you do not specify /sysarray when you run tlbimp:

 testproject.udtEmployee[] [MarshalAs(safearray record)]         TestSafeArrayOfUDTs () { } 

The Employee UDT becomes a value type with the same name in the Interop assembly and the TestSafeArrayofUDTs method returns an array of these value types. You can call this method using the example code shown here:

 int i; string strName; TestClass myClass = new TestClass(); udtEmployee[] arr=myClass.TestSafeArrayOfUDTs(); for (i=0;i<arr.Length;i++)       strName=arr[i].Name; 

If you do use the /sysarray parameter, the managed code equivalent of the TestSafeArrayofUDTs method will have the following prototype:

 System.Array [MarshalAs(safearray record)]         TestSafeArrayOfUDTs () { } 

You can call this version of the method using the following code:

 int i; string strName; udtEmployee emp; TestClass myClass = new TestClass(); System.Array arr=myClass.TestSafeArrayOfUDTs(); for (i=0;i<arr.Length;i++) {       emp=(udtEmployee)arr.GetValue(i);       strName=emp.Name; } 

In this case, the managed version of the method returns a System.Array that contains instances of the udtEmployee value type.

I'll next look at the TestSafeArrayAsParameter1 method; this method is declared as follows in the IDL for the COM object.

 [id(0x60030003)] HRESULT TestSafeArrayAsParameter1([in, out] SAFEARRAY(BSTR)* strs); 

The parameter for this method is an array of strings, and it is a by-reference parameter, that is, the method accepts the parameter as input from its caller. However, it may also alter the array, and the client will see the method's updates. This is as opposed to a by-value parameter, which will have the IDL [in] attribute only, and any changes that the method makes to the parameter will be invisible to the client. Array parameters can only be by-reference in VB. If you attempt to create a by-value array parameter, you will receive the following compiler error:

 Array argument must be ByRef 

If I do not use the /sysarray parameter, the managed code equivalent of this method is as follows:

 void  TestSafeArrayAsParameter1([ref] string[] [MarshalAs(safearray bstr)] strs); 

In this case, the parameter is declared in C# as a reference ([ref]) parameter; this is the way you declare in/out parameters in C#. You can call this version of the TestSafeArrayAsParameter1 method using the following code:

 int i; string strValue; TestClass myClass = new TestClass(); string[] myStrs=new String[3]; myStrs[0]="Whatever"; myStrs[1]="It"; myStrs[2]="Takes"; myClass.TestSafeArrayAsParameter1(ref myStrs); 

If the TestSafeArrayAsParameter1 method changed the contents of the array that was passed in, its caller will see the changes. If you use the /sysarray parameter, the Type Library Importer will generate the following managed code equivalent for the method.

 void TestSafeArrayAsParameter1([ref] System.Array [MarshalAs(safearray bstr)] strs) { } 

The final method is the TestSafeArrayAsParameter2 method, which is similar to TestSafeArrayAsParameter1 except it takes a SafeArray of udtEmployee objects instead of strings. If I do not use the /sysarray parameter, the managed code equivalent of this method is as follows:

 void TestSafeArrayAsParameter2([ref] valuetype testproject.udtEmployee[]         [MarshalAs(safearray record)] Employees)  {  } 

In this case, it converts the SafeArray of UDTs to an array of equivalent value types. The MarshalAs attribute will convert the array to a SafeArray of Records, which is how you represent a Safe Array of user-defined types. You can call this method from a C# client as follows:

 int i; string strName; udtEmployee emp; TestClass myClass = new TestClass(); udtEmployee[] empList=new udtEmployee[2]; empList[0].ID=10; empList[0].Name="Alan Gordon"; empList[0].Salary=45; empList[1].ID=20; empList[1].Name="Bria Gordon"; empList[1].Salary=60; myClass.TestSafeArrayAsParameter2(ref empList); 

If I use the /sysarray parameter, the managed code equivalent of this method will be as follows:

 void  TestSafeArrayAsParameter2([ref] System.Array [MarshalAs(safearray record,"testproject.udtEmployee")] Employees)  { } 

Notice that tlbimp converts the parameter to an instance of System.Array, and, once again, the MarshalAs attribute specifies that the array should be converted to a SafeArray of udtEmployee instances. You can call this method from a C# client as follows:

 int i; string strName; udtEmployee emp; TestClass myClass = new TestClass(); udtEmployee[] empList=new udtEmployee[2]; empList[0].ID=10; empList[0].Name="Alan Gordon"; empList[0].Salary=45; empList[1].ID=20; empList[1].Name="Bria Gordon"; empList[1].Salary=60; System.Array arr=empList; myClass.TestSafeArrayAsParameter2(ref arr); 
C-STYLE ARRAYS

As I mentioned earlier, SafeArrays are easy to use from VB, so they are the preferred way to pass arrays across a COM Interface that is implemented in Visual Basic or will be called from a VB client. If you are implementing a COM object in C++ and the object will only be called from C++ clients , SafeArrays are cumbersome to work with. Fortunately, IDL supports the capability to use C-style arrays. These arrays are much easier to work with if both the COM client and server are implemented in C++. There are three types of C-style arrays: (1) fixed arrays, (2) conformant arrays, and (3) varying arrays. To illustrate the concepts in this section, I created another COM server using Visual C++ and ATL. The IDL for this server is the following:

 import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(1A39EB1D-862F-4109-9E13-C6C8301F337A), dual ] interface ICStyleArrayTest : IDispatch {     [id(1)] HRESULT TestFixed([in]  short  arg1[5], [in] short arg2[5][5]);     [id(2)] HRESULT TestVarying1D([in] short start,[in] short length,         [in, first_is(start),length_is(length)]         short arg[5]);     [id(3)] HRESULT Conformant1D([in] short length,         [in, size_is(length)] short *arg);     [id(4)] HRESULT Conformant2D([in] short rows,         [in] short columns,         [in, size_is(rows,columns)] short **arg);     [id(5)] HRESULT EmployeeTest1([in] short numEmps,         [in,size_is(numEmps)] Employee *employees);     [id(6)] HRESULT EmployeeTest2([in,out] short *numEmps,         [out] Employee **employees); }; [     uuid(308E9B90-59E2-4D53-A56F-9E3BF40AF173),     version(1.0) ] library CSTYLEARRAYPROJECTLib { importlib("stdole32.tlb"); importlib("stdole2.tlb"); [     uuid(ABD60198-74F7-4955-AC38-016F8ADA2D82) ] coclass CStyleArrayTest {     [default] interface ICStyleArrayTest; }; }; 
Fixed Arrays

The following IDL code shows an example of a method that takes a fixed array as a parameter:

 [id(1)] HRESULT TestFixed([in]  short  arg1[5], [in] short arg2[5][5]); 

The managed code equivalent of this method looks as follows:

 void TestFixed([in] int16[]  MarshalAs([5]) arg1,                  [in] int16[]  MarshalAs([25]) arg2); 

In the managed code version of the method, the second argument, which is a 5x5 2D array, is converted to a 25 element, one-dimensional (1D) array in row-major order. The Interop Marshaler is not currently able to support fixed-length arrays with more than one dimension, so it flattens the 2D array into a 1D array. The following code shows how you would call the TestFixed method from managed code:

 1.  private void button3_Click(object sender, 2.    System.EventArgs e) 3.  { 4.    short i, j, numRows, numCols; 5.    numRows=5; 6.    numCols=5; 7.    short[] arg1=new short[5]; 8.    short[] arg2=new short[numRows*numCols]; 9.    for (i=0;i<5;i++) 10.       arg1[i]=(short)(5*i); 11.   for (i=0;i<numRows;i++) 12.       for (j=0;j<numCols;j++) 13.       arg2[i* numCols+j]=(short)(2*i + 3*j); 14.   CStyleArrayTest obj=new CStyleArrayTest(); 15.   obj.TestFixed(arg1,arg2); 16. } 

On line 4, I declare several variables. On lines 5 through 8, I initialize the variables that store the number of rows and columns for the 2D arrays, and I also initialize the arrays that I will pass to the TestFixed method. On lines 9 and 10, I initialize the 1D array, and, on lines 11 through 13, I initialize the 2D array. On line 14, I create an instance of CStyleArrayTest, and, on line 15, I call the TestFixed method. Fixed arrays are by far the easiest c-style arrays to use. Let's look at conformant arrays, which are a little more difficult to work with from managed code.

Conformant Arrays

A conformant array is one where the size of the array is specified at runtime. The following IDL code shows an example of a method that takes a 1D conformant array as its second parameter:

 [id(3)] HRESULT Conformant1D([in] short arrlen,       [in, size_is(arrlen)] short *arg); 

Notice that I use the size_is attribute to specify that the size of the array is passed in the first parameter. The managed code equivalent of this method is shown here:

 void Conformant1D(int16 length,ref int16 arg); 

You can see that the managed code version of the method is just plain wrong. The second parameter is not typed as an array, but is typed as a by-reference (ref) argument to a single, short instance. This will only work for a single-element array.

If you dig a little deeper, you can see that the tlbimp is not at fault. The real problem is the COM type library. Take a look at the definition of the Conformant1D method as it appears in the COM object's type library.

 [id(0x00000003)] HRESULT Conformant1D([in] short arrlen,[in] short* arg); 

Notice that all of the size information for the second argument is lost. Because the Type Library Importer uses the type library as its input, it has no way of knowing if the second parameter is a pointer to a single instance (for example, a by-reference argument) or if it represents an array. Furthermore, the connection between the first and second parameterthat the first parameter specifies the length of the array that is passed in the second parameteris also lost. The managed code declaration of the Conformant1D method in the Interop Assembly is only useful if you are passing an array that contains a single element. The only solution to this problem is to edit the method signature in the Interop assembly manually. The process for doing this is shown in Figure 7-10.

Figure 7-10. The process for manually editing an Interop assembly.

graphics/07fig10.gif

You start by using ILDASM to save the IL code for your Interop assembler into a text file. You then edit the code using your favorite text editor. You then reassemble the code back into an assemblywith the same nameusing the IL Assembler ( ILASM ). Both of these IL tools ship with the .NET Framework SDK.

Let's follow this example through and actually alter the Interop assembly so that the Conformant1D method will work properly. Earlier I created a COM object that contains all of the methods that I've talked about so far: Conformant1D, TestFixed, and so forth. I created an Interop assembly for this COM server by running the following command:

 tlbimp cstylearrayproject.dll /out:dotnetcstylearray.dll 

Note

When I ran this command, the tlbimp generated several warnings with a message similar to the following: "Tlbimp warning: At least one of the arguments for '[MethodName]' can not be marshaled by the runtime marshaler. Such arguments will therefore be passed as a pointer and may require unsafe code to manipulate". Where "[MethodName]" is the name of a method in the type library. Whenever you run tlbimp and it encounters a parameter that it cannot convert to a managed code equivalent, it throws up its hands and converts it to a native pointer. A native pointer is an instance of System.IntPtr. System.IntPtr is a platform-specific type that is used to hold pointers and handles. The size of a System.IntPtr is the size of a memory address on the platform, that is, 32 bits on a 32-bit platform and 64 bits on a 64-bit platform. You then have to write your own code to marshal native pointer parameters correctly. I will show you how to do this shortly.


If you want to alter the generated Interop assembly, you will first need to open the assembly in ILDASM by running the following command:

 ildasm dotnetcstylearray.dll 

You can then save the MSIL code for the Interop assembly by performing the following steps:

  1. Select File Dump.

  2. Accept the default Dump options by clicking OK. A File SaveAs dialog will appear.

  3. Select the location where you want to save the file to.

  4. Click the Save button.

For this example, save the MSIL code to a file called cstyleproject.il . Now, find the Conformant1D method in the MSIL code; it should look like the following:

 instance void  Conformant1D([in] int16 length,                             [in] int16& arg) 

Change this definition to look as follows:

 instance void  Conformant1D([in] int16 length,                         [in] int16[] marshal([+0]) arg) 

Here I am using the marshal attribute with square brackets "[ ]" to specify that the second parameter is actually an array.

Note

The marshal attribute in MSIL maps to the MarshalAs attribute in C#.


You can use the marshal attribute in two ways: (1) You can specify a fixed number of elements that the array will contain (tlbimp used this form of the marshal attribute for the TestFixed method), and (2) you can specify the zero-based index (reading from left to right) of the parameter that contains the size of the array. This is called a parameter index, and it is what I just used for the Conformat1D method. To specify a parameter index, you must preceed the index value with a "+" symbol. In this case, I specified that the length of the second parameter is contained in the first parameter.

You will need to alter the MSIL code in two places. First there is the Conformant1D method declaration in the ICStyleArrayTest interface and then there is the implementation of the method in the CStyleArrayTestClass class. Make sure that you save the MSIL file with the updated definition of the Conformant1D method. You can now recompile the MSIL using the MSIL Assembler by issuing the following command:

 ilasm /OUT=dotnetcstylearray.dll cstyleproject.il /DLL 

The assembly called dotnetcstylearray.dll will now contain the updated definition of the Interop assembly. Make sure you give this assembly the same name as the original assembly that it was built from. If you are not sure what this name should be, you can find it in the IL by looking for the assembly declaration near the top of the IL file.

 .assembly dotnetcstylearray {   ... } 

You can now create a C# client, reference the new version of the assembly, and call the Conformant1D method using the following code:

 private void btn1_Click(object sender, System.EventArgs e) {        CStyleArrayTest obj=new CStyleArrayTest();        short[] arr=new short[5];        short i;        for (i=0;i<5;i++)          arr[i]=i;        obj.Conformant1D(5,arr); } 

Unfortunately, the modification that I made to the Conformant1D method will not work if the array parameter is an in/out argument, that is, an argument that the COM method may change and you want the managed client to see the changes. The following IDL code shows a version of the Conformant1D method where the second parameter is in/out:

 [id(3)] HRESULT Conformant1D([in,out] short *length,        [in, out, size_is(*length)] short **arg) 

Notice that the second parameter is declared as a pointer to a pointer. If you run the type library importer on a type library that contains this updated version of the Conformant1D method, you get the following managed code equivalent in MSIL:

 instance void  Conformant1D([in][out] int16& length,                             [in][out] native int arg) 

The C# equivalent of this managed code looks as follows:.

 void Conformant1D(ref short length,                   System.IntPtr arg); 

Notice that the second parameter to the Conformant1D method is now a native pointer (System.IntPtr). A native pointer is a platform-specific type that is used to hold pointers to unmanaged memory and handles. The size of a native pointer is the size of a memory address on the platform, that is, 32 bits on a 32-bit platform or 64 bits on a 64-bit platform. The type library importer will convert a parameter or return value to a System.IntPtr whenever it is unable to figure out how to marshal the parameter or return value to a managed type. This is effectively the type library importer's way of saying: "I don't know what to do with this, you figure it out!" You will have to marshal the parameter or return value manually. Fortunately, the System.Runtime.InteropServices.Marshal class contains all the tools you need to marshal managed types to unman aged types and vice versa. Before we can implement managed marshaling we have to fix a problem with the interop assembly however.

The problem is that the second parameter is declared as a pointer to a pointer in the original IDL code. The Type Library Importer has no way of knowing if the parameter is supposed to be a 2D array or a by-reference 1D array. Therefore, if it sees a pointer with more than one level of indirection (short **arg), it converts it to a native pointer. It's up to you to know whether the parameter is a 2D array or a by-reference 1D array.

In this case, because you know that the parameter is a by-reference 1D array, you should convert the second parameter to a by-reference argument by adding an ampersand "&" to its MSIL declaration as follows:

 instance void  Conformant1D([in][out] int16& length,                             [in][out] native int  &  arg) 

To make this change, you must follow the same steps that you performed earlier:

  1. Load the assembly into ILDASM.

  2. Select File Dump to save the MSIL file.

  3. Modify the Conformant1D method.

  4. Compile the MSIL file into the assembly using ILASM.

The C# version of this method will now look as follows in the interop assembly:

 void Conformant1D(ref short length,                   ref System.IntPtr arg); 

Notice that the native int second parameter is now passed by-reference. You will still have to manually marshal this parameter from an array of shorts (System.Int16) on the managed side to a physical pointer to an array of shorts on the unmanaged side. Because a managed array of shorts is a "blittable" type (see the sidebar on "blittable" types in this section), there are 2 ways that you can do this manual marshaling. The first way is to "pin" the managed array in memory and then obtain the physical address of the array and pass it to the Conformant1D method. When you "pin" a managed object in memory you prevent the garbage collector from changing the physical memory address of the object so it is safe to pass the address directly to an umanaged method that expects a pointer. This approach has the highest performance, because you don't have to copy any data from managed to unmanaged buffers, but it will only work if the managed array contains "blittable" types. The second approach is to allocate an unmanaged buffer and then copy the contents of the managed array into the unmanaged buffer. You can then pass the address of this unmanaged buffer in the second parameter to the Conformant1D method. This approach will always work regardless of the type of the managed data that you are passing to the method but it is slower.

Blittable Types

A type is blittable if it has a common representation in both managed and unmanaged memory. When it is doing the marshaling for you, the Interop Marshaler will pin instances of blittable types and pass the address directly to unmanaged methods instead of copying the type to unmanaged memory; this improves performance. You should take the same approach when you are forced to implement manual marshaling. A list of blittable types is shown below.

  • System.Byte

  • System.SByte

  • System.Int16

  • System.UInt16

  • System.Int32

  • System.UInt32

  • System.Int64

  • System.IntPtr

  • System.UintPtr

  • A value type that contains fields of blittable types

  • A one dimensional array of blittable types

The array of shorts (System.Int16) that I pass as the second parameter to the Conformant1D method is blittable. The second parameter to this version of the Conformant1D method is also an in/out parameter. Therefore, it will populate data in the array on return and it may even allocate a new buffer for the array and re-dimension it. You also have 2 choices as to how to marshal the output of the Conformant1D method. Your first choice is to use the methods in the System.Runtime.InteropServices.Marshal class to read the elements directly from the unmanaged array. Your second choice is to copy the data in the unmanaged buffer to a managed array.

Let's look at some managed code that calls the Conformant1D method. Since the second parameter to this method is blittable, let's look at pinning the object first.

 1.  private void button1_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      short i, arrayLength, result; 5.      short[] arr=new short[5]; 6.      GCHandle handle= 7.        GCHandle.Alloc(arr,GCHandleType.Pinned); 8.      IntPtr ptr=handle.AddrOfPinnedObject(); 9.      try 10.     { 11.          arrayLength=5; 12.          for (i=0;i<5;i++) 13.              arr[i]=i; 14.          CStyleArrayTest obj=new CStyleArrayTest(); 15.          obj.Conformant1D(ref arrayLength,ref ptr); 16.          int sz=Marshal.SizeOf(typeof(System.Int16)); 17.          for (i=0;i<arrayLength;i++) 18.          result=Marshal.ReadInt16(ptr,i*sz); 19.     } 20.     finally 21.     { 22.       if (handle.IsAllocated) 23.         handle.Free(); 24.     } 25. } 

On lines 1 through 5 I have some pretty standard variable declarations. On line 5 I declare an array of 5 shorts (System.Int16); this is the variable that I will pass as the second parameter to the Conformant1D method. On lines 6 and 7 I declare and initialize an instance of the System.Runtime.InteropServices.GCHandle type. The GCHandle type, which is declared a value-type structure, is used to access a managed object from unmanaged memory. In this case, I initialize the GCHandle instance to provide unmanaged access to the managed array of shorts that I declared on line 5. Notice that the array is the first parameter to the GCHandle.Alloc method. The second parameter to this method indicates that I wish to "pin" the array in memory, which will prevent the garbage collector from changing the physical address of this variable if a garbage collection occurs. On line 8 I call the AddrOfPinnedObject method on the GCHandle object to get the physical address of the managed array. This value is stored in an IntPtr instance called ptr. On lines 11 through 13 I populate the managed array. On line 14 I create a CstyleArrayTest object and on line 15 I call the Conformant1D method on this object passing in the IntPtr variable that contains the address of the managed array as the second parameter. On line 16 I get the size of each element in the managed array, which is 2 bytes for a short, and then on lines 17 and 18 I use the ReadInt16 method on the System.Runtime.InteropServices.Marshal class to read the elements of the unmanaged array that the Conformant1D method returned. The first parameter to the ReadInt16 method is a pointer to the beginning of the unmanaged buffer. The second parameter is an offset that is added to the first parameter before reading.

Note

The Marshal class contains a series of methods for reading data from an unmanaged buffer. In addition to the ReadInt16 method, there are also ReadByte, ReadInt32, ReadInt64, and ReadIntPtr methods. The Marshal class also contains a set of matching methods for writing to an unmanaged buffer. These methods have the name WriteByte, WriteInt16, etc.


Finally, on lines 22 and 23 I free the memory handle. Notice that I put this logic in a "finally" clause so that this code will run regardless of whether an exception was thrown.

Now lets look at code that calls the Conformant1D method by copying the managed, input array to an unmanaged buffer. This approach will work even with non-blittable types.

 1.  private void button1_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      short i, arrayLength, rslt; 5.      short[] arr=new short[5]; 6.      IntPtr ptr=IntPtr.Zero; 7.      try 8.      { 9.        arrayLength=5; 10.       for (i=0;i<5;i++) 11.         arr[i]=i; 12.       ptr=Marshal.AllocCoTaskMem(13.         Marshal.SizeOf(Type.GetType(14.         "System.Int16"))*arrayLength); 15.       Marshal.Copy(arr,0,ptr,arrayLength); 16.       CStyleArrayTest obj=new CStyleArrayTest(); 17.       obj.Conformant1D(ref arrayLength,ref ptr); 18.       arr=new short[arrayLength]; 19.       Marshal.Copy(ptr,arr,0,arrayLength); 20.       for (i=0;i<5;i++) 21.         rslt=arr[i]; 22.     } 23.     finally 24.     { 25.       Marshal.FreeCoTaskMem(ptr); 26.     } 27. } 

Lines 4 through 6 contain variable declarations. Notice the declaration of a managed array called arr on line 5 and the declaration of a native pointer (System.IntPtr) called ptr on line 6. On line 10 and 11, I populate the array that I declared on line 5. On lines 12 through 14, I call the AllocCoTaskMem method on the System.Runtime.InteropServices.Marshal class to allocate unmanaged memory for the native pointer that I declared on line 6. The AllocCoTaskMem method is just a wrapper on the CoTaskMemAlloc method in the COM API. On line 15, I call the Copy method on the System.Runtime.InteropServices.Marshal class to copy the contents of the managed array to the unmanaged memory block that the native pointer references.

There are two sets of Copy methods in the System.Runtime.InteropServices.Marshal class for a total of 14 methods in all. Seven methods copy data from a managed array to an unmanaged memory buffer. These methods are declared as follows:

 public static void Copy(short[] src,int startIndex,       IntPtr dest,int length); 

The only difference between these methods is the type of the source managed array. There are versions of this method that take byte, char, short (System.Int16), int (System.Int32), long (System.Int64), float (System.Single), and double (System.Double). The other seven Copy methods allow you to perform the reverse operation, that is, to copy from an unmanaged memory buffer to a managed array of any of the aforementioned types. An example of one of these methods is as follows:

 public static void Copy(IntPtr src,short[] dest,       int startIndex,int length); 

On line 16 of the source code example, I instantiate a CstyleArrayTest object, and then, on line 17, I call the Conformat1D method passing in the ArrayLength variable that holds the size of the array and the ptr variable that holds the native pointer. Remember that the second argument is a in/out or ref parameter, and the Conformant1D method may change the size and contents of the array and pass the result back to its caller in the second parameter. On lines 18 and 19, I show how to marshal the modified array from the COM component back into a managed array on the client. On line 18, I allocate a new managed array using the arrayLength that is returned from the Conformant1D method. Remember that if the Conformant1D changes the size of the array, it is responsible for deallocating the memory for the unmanaged array that was passed in and allocating a new, unmanaged buffer for the re-dimensioned array. On line 19 I copy the contents of the ptr native pointer that is returned as an output parameter from the Conformant1D method to the managed array that I initialized on line 18. Keep in mind that I didn't have to copy the unmanaged data to an managed buffer. I could have used the ReadInt16 to read directly from the unmanaged buffer as I did previously. I only used this approach here for pedagogical reasons to demonstrate both ways of reading unmanaged memory. On lines 20 and 21, I loop through the contents of the array to verify that the contents were copied correctly; this code is obviously not strictly necessary. Finally, on line 25, I free the unmanaged buffer that was returned from the Conformant1D method using the FreeCoTaskMem on the System.Runtime.InteropServices class. The FreeCoTaskMem method maps to the CoTaskMemFree method in the COM API.

Let's now look at how a COM method with a 2D conformant array parameter gets mapped to managed code. The following IDL code shows an example of a method that takes a 2D conformant array as an input parameter:

 [id(4)] HRESULT Conformant2D([in] short rows, [in] short columns, [in, size_is(rows,columns)] short **arg); 

We specify the number of rows and columns in the array using the size_is parameters. The values passed to the size_is are specified in the first and second parameters of the method. Notice that the third parameter is a pointer to a pointer, which is the same as the by-reference 1D array. The following code shows the managed code equivalent of the Conformant2D method in MSIL code:

 instance void  Conformant2D([in] int16 rows,      [in] int16 columns,      [in] native int arg) 

Here is the same code in C#:

 void  Conformant2D(short rows,      short columns,      System.IntPtr arg) 

As I mentioned earlier, the Type Library Importer will always convert any pointers with two or more levels of indirection into a native pointer. The only difference between the way that this function works and the Conformant1D method is the way that you handle the third parameter. Notice that the array parameter in this method also gets converted into an IntPtr.

The .NET Framework SDK documentation says that, if you edit the IDL definition of your object to look as follows:

 instance void  Conformant2D([in] int16 rows,      [in] int16 columns,      [in] int16[,] marshal([]) arg) 

You should be able to pass a 2D managed array directly to the method as shown here:

 private void button2_Click(object sender,System.EventArgse) {       short i, j, numRows, numCols;       numRows=5;       numCols=5;       short[,] arr=new short[numRows,numCols];       for (i=0;i<numRows;i++)       for (j=0;j<numCols;j++)         arr[i,j]=(short)(2*i + 3*j);       CStyleArrayTest obj=new CStyleArrayTest();       obj.Conformant2D(numRows,numCols,arr); } 

I was able to compile the Interop assembly with the altered definition, but, when I tried to call the Conformant2D method, I would always receive the following runtime error:

 "Object reference not set to an instance of an object" 

The error occurs when the COM object tried to access the array that the managed client passes to it, so I abandoned this approach. Using the original Interop assembly generated by tlbimp, I was able to pass a 1D equivalent of a 2D array to the Conformant2D method using the logic shown here:

 1.  private void button2_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      short i, j, numRows, numCols; 5.      IntPtr ptr,ptr2ptr, ptrIter, ptr2ptrIter; 6.      numRows=5; 7.      numCols=5; 8.      short[] arr=new short[numRows*numCols]; 9.      for (i=0;i<numRows;i++) 10.       for (j=0;j<numCols;j++) 11.           arr[i*numCols+j]=(short)(2*i+3*j); 12.     ptr2ptr=Marshal.AllocCoTaskMem(13.       Marshal.SizeOf(14.       Type.GetType("System.IntPtr"))*numRows); 15.     ptr=Marshal.AllocCoTaskMem(16.       Marshal.SizeOf(17.       Type.GetType("System.Int16"))*numCols*numRows); 18.     Marshal.Copy(arr,0,ptr,numRows*numCols); 19.     ptr2ptrIter=ptr2ptr; 20.     ptrIter=ptr; 21.     for (i=0;i<numRows;i++) 22.     { 23.       Marshal.StructureToPtr(24.         ptrIter,ptr2ptrIter,false); 25.       ptr2ptrIter=(IntPtr)(26.         (int)ptr2ptrIter+System.IntPtr.Size); 27.       ptrIter=(IntPtr)(28.         (int)ptrIter+Marshal.SizeOf(29.           Type.GetType("System.Int16"))*numCols); 30.     } 31.     CStyleArrayTest obj=new CStyleArrayTest(); 32.     obj.Conformant2D(numRows,numCols,ptr2ptr); 33.     Marshal.FreeCoTaskMem(ptr2ptr); 34.     Marshal.FreeCoTaskMem(ptr); 35. } 

Essentially , what I am doing here is converting a 1D managed array to the memory layout of a C/C++ 2D array. On lines 4 and 5, I declare variables. On lines 6 and 7, I initialize the variables that contain the number of rows and columns of the array. On line 8, I declare and initialize the 1D managed array that will be the managed equivalent of the 2D unmanaged array that I will pass to the COM object. On lines 9 through 11, I initialize the contents of the array. Notice how I use the expression i*numCols+j to index the array in row-major order. 2D arrays in C and C++ are arranged as an array of pointers to pointers with a length equal to the number of rows. Each element in the row array points to an array whose length equal to the number of columns. On lines 12 through 14, I allocate unmanaged memory for the row array of pointers to pointers. The elements of this array are native pointers, that is, System.IntPtr instances. On lines 15 through 17, I allocate unmanaged memory for all the data in the array. The size of this buffer is the number of rows times the number of columns times the size of each element, which is 2 bytes for a short. Rather than allocate a separate buffer for each row, I instead allocated a single buffer, and I'll point the pointers in the row array to the memory location that corresponds to the beginning of each row. On line 18, I copy the entire contents of the 1D managed array to the unmanaged buffer. All I need to do now is to point the row array of pointers to pointers at the right locations within this buffer. On lines 19 and 20, I initialize the variables that I will use to loop through the row and data buffers. On lines 21 through 30, I loop for each row and store the address of the beginning of each row into the elements of the row array of pointers to pointers. I use a method of the Marshal class called StructureToPtr to get these addresses. The StructureToPtr method has the following declaration in C#:

 public static void StructureToPtr(object structure,       IntPtr ptr,bool fDeleteOld); 

This method is usually used to copy a structure to an unmanaged buffer. The first parameter is the structure, and the second parameter is a pointer to the unmanaged buffer. The third parameter is a Boolean that allows you to specify whether the StructureToPtr method should call the DestroyStructure method on the ptr variable before it sets its value equal to the contents of the structure that is passed in the first parameter. In this case, the structure is an instance of System.IntPtr . When you call the StructureToPtr method and pass in a System.IntPtr , you are assigning the pointer in the second parameter to the address contained in the first parameter, that is, you are essentially adding a level of indirection or creating a pointer to a pointer. On lines 25 through 29, I increment the row array pointer and the data array pointer so that I can set up the next row. You cannot perform pointer arithmetic directly on an instance of System.IntPtr , but the System.IntPtr class does support a type conversion to System.Int . Therefore, if you want to perform pointer arithmetic, you can cast the System.IntPtr to a System.Int , perform your arithmetic, and then cast the result back to a System.IntPtr . This is exactly what I did on lines 25 through 29. On line 31, I create an instance of the CStyleArrayTest class, and then, on line 32, I call the Conformant2D method passing in a System.IntPtr instance that points to the beginning of the row array of pointers to pointers. On lines 33 and 34, I free the memory allocated to these two buffers.

The last thing that I will look at before I leave the difficult subject of marshaling c-style arrays is marshaling an array of user-defined structures. Let's look at the EmployeeTest1 method in the COM object, which has the following IDL definition:

 [id(5)] HRESULT EmployeeTest1([in] short numEmps, [in,size_is(numEmps)] Employee *employees); 

The second parameter to this method is an array of Employee structures. The Employee structure is defined in IDL as follows:

 typedef struct tagEmployee {       SHORT ID;       BSTR Name;       CURRENCY Salary; } Employee; 

The tlbimp will convert the Employee structure into a Value type called tagEmployee. The tlbimp will also generate the managed code equivalent of the EmployeeTest1 method shown here in MSIL:

 instance void  EmployeeTest1([in] int16 numEmps,       [in] valuetype dotnetcstylearraynew.tagEmployee& employees) 

This version of EmployeeTest1 has the same problem as the Conformant1D method earlier. The tlbimp cannot tell if the second parameter is a pointer to a single instance or an error, so it generates a managed code equivalent that assumes the parameter is a pointer to a single instance. As I did with the Conformant1D method, I have to change the definition of the method so that it is treated as an array. I need to follow the same procedure that I did for the Conformant1D method, that is, to change

 ildasm dotnetcstylearray.dll 

You can then save the MSIL code for the Interop assembly by performing the following steps:

  1. Load the Interop assembly into the ILDASM by executing the following command: ildasm dotnetcstylearray.dll.

  2. Select File Dump.

  3. Accept the default Dump options by clicking OK. A File SaveAs dialog will appear.

  4. Select the location where you want to save the file to.

  5. Click the Save button.

  6. Modify the EmployeeTest1 method to marshal the second parameter as an array in two places: (1) the CStyleArrayTestClass class and the (2) the ICStyleArrayTest interface.

  7. Recompile the assembly by executing the following command: ilasm /OUT=dotnetcstylearray.dll cstyleproject.il /DLL.

You should have changed the EmployeeTest1 method to look as follows:

 void  EmployeeTest1([in] int16 numEmps,     [in] valuetype dotnetcstylearraynew.tagEmployee[]     marshal([+0]) employees) 

Now you can write the following client code to call the EmployeeTest1 method:

 1.  private void TestEmp_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      CStyleArrayTest obj=new CStyleArrayTest(); 5.      tagEmployee[] employees=new tagEmployee[3]; 6.      employees[0].ID=5; 7.      employees[0].Name="Alan Gordon"; 8.      employees[0].Salary=2400; 9.      employees[1].ID=15; 10.     employees[1].Name="Tamber Gordon"; 11.     employees[1].Salary=4800; 12.     employees[2].ID=25; 13.     employees[2].Name="Bria Gordon"; 14.     employees[2].Salary=9600; 15.     obj.EmployeeTest1(3,employees); 16. } 

The code here is pretty simple. On line 4, I create an instance of the CstyleArrayTest class. On line 5, I declare an array of tagEmployees with length 3. On lines 6 through 14, I populate the array, and, on line 15, I call the EmployeeTest1 method.

Now let's take a look now at a very common scenario with arrays, that is, a method that returns an array of user-defined structures as an output parameter. The IDL of just such a method, EmployeeTest2, is shown here:

 [id(6)] HRESULT EmployeeTest2([in,out] short *numEmps,       [out] Employee **employees); 

The tlbimp converts this to the following managed code equivalent in MSIL:

 instance void  EmployeeTest2([in][out] int16& numEmps,                 [out] native int employees) 

Currently, the managed code version of the EmployeeTest2 method will not work. Here, I have run into the problem that the tlbimp has in distinguishing between a by-reference 1D array (which is what I have here) and a 2D array. I have to change the second parameter to be a reference argument in the MSIL. I won't repeat the steps to do this because I have done it a few times now. However, you will need to edit the MSIL definition of the EmployeeTest2 method to look as follows and then recompile the code back into an assembly:

 instance void  EmployeeTest2([in][out] int16& numEmps,                 [out] native int& employees) 

Don't forget to change the method definition in both the CStyleArrayTestClass class and the ICStyleArrayTest interface. You can now write the following client code to call this method:

 1.  private void TestEmp_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      short i, numEmps=4; 5.      IntPtr ptr; ptrIter 6.      int empSize; 7.      ptr=System.IntPtr.Zero; 8.      CStyleArrayTest obj=new CStyleArrayTest(); 9.      obj.EmployeeTest2(ref numEmps,out ptr); 10.     tagEmployee[] outEmps=new tagEmployee[numEmps]; 11.     empSize=Marshal.SizeOf(typeof(tagEmployee)); 12.     ptrIter=ptr; 13.     for (i=0;i<numEmps;i++) 14.     { 15.       outEmps[i]=(tagEmployee) 16.         Marshal.PtrToStructure(17.             ptrIter,typeof(tagEmployee)); 18.       ptr=(IntPtr)((int)ptr+empSize); 19.     } 20.     Marshal.FreeCoTaskMem(ptr); 21. } 

On lines 4 through 6, I have variable declarations. On line 4, I initialize the number of Employee objects that I would like to receive from the EmployeeTest2 method. On line 7, I initialize the unmanaged pointer that I will receive from the method to NULL (IntPtr.Zero). On line 8, I instantiate a CStyleArrayTest object, and, on line 9, I call the EmployeeTest2 method. On line 10, I instantiate a managed array of the tagEmployee value type that will hold the array that I receive from the method. On line 11, I calculate the size of each instance of tagEmployee. On line 12, I initialize an IntPtr variable that I will use to iterate through the unmanaged buffer that the EmployeeTest method returned. On lines 13 through 19, I fill the managed array with the contents of the buffer that is returned by the EmployeeTest2 method. On lines 15 through 17, I call the PtrToStructure method in the System.Runtime.InteropServices.Marshal class. This function takes two parameters: a native pointer that contains the unmanaged version of the structure and a System.Type object that contains the type object for the managed version of the structure. On line 18, I increment the native pointer so that it points to the next tagEmployee instance in the returned buffer. The calling is responsible for freeing the memory that is returned by output parameters on a COM method. On line 20, I free the memory buffer that the EmployeeTest2 function returned.

Varying Arrays

COM (IDL really) varying arrays allow you to pass a dynamic slice of a fixed array. The following method, defined here in IDL, uses a varying array:

 [id(2)] HRESULT TestVarying1D([in] short start, [in] short length, [in, first_is(start),length_is(length)] short arg[5]); 

In this case, the first and second parameters to the method allow you to specify the starting index and the number of elements from that starting point that will be passed from the client to the server. You specify the starting index using first_is and the number of elements to copy over using length_is. You could instead use first_is and last_is to specify the starting and ending indexes to copy from the client to the server as follows:

 [id(2)] HRESULT TestVarying1D([in] short start, [in] short end, [in, first_is(start),last_is(end)] short arg[5]); 

The tlbimp will convert either one of these IDL definitions to the following managed code equivalent:

 instance void  TestVarying1D([in] int16 start, [in] int16 length,[in] native int arg) 

Notice that the third parameter is a native pointer. My own experiments showed that the tlbimp generates a native pointer for an array if you use the first_is, last_is, or length_is IDL attributes on the array. You can use the following logic in a C# client to call the method without making any changes to the Interop assembly:

 1.  private void button3_Click(object sender, 2.      System.EventArgs e) 3.  { 4.      short i, start, length, arrSize=5; 5.      short[] arg1=new short[arrSize]; 6.      System.IntPtr ptr, ptrStart; 7.      start=2; 8.      length=2; 9.      for (i=0;i<5;i++) 10.         arg1[i]=(short)(arrSize*i); 11.     ptr=Marshal.AllocCoTaskMem(12.         Marshal.SizeOf(typeof(short))*arrSize); 13.     ptrStart=(IntPtr) 14.       ((int)ptr+start*Marshal.SizeOf(typeof(short))); 15.     Marshal.Copy(arg1,start,ptrStart,length); 16.     CStyleArrayTest obj=new CStyleArrayTest(); 17.     obj.TestVarying1D(start,length,ptr); 18.     Marshal.FreeCoTaskMem(ptr); 19. } 

On lines 4 through 6, I declare variables. On lines 7 and 8, I initialize the starting index and the length for the array that I will pass to the TestVarying1D method. On lines 9 and 10, I populate a 1D array. On lines 11 and 12, I allocate a buffer of unmanaged memory that is the same size as the array. On lines 13 and 14, I set the starting point in the unmanaged buffer. On line 15, I call the Copy method on the System.Runtime.InteropServices.Marshal class to copy the contents of the array to the unmanaged buffer. On line 16, I instantiate a CstyleArrayTest object, and then, on line 17, I call the TestVarying1D method. Finally, on line 18, I call FreeCoTaskMem to free the unmanaged buffer.


Team-Fly    
Top
 


. Net and COM Interoperability Handbook
The .NET and COM Interoperability Handbook (Integrated .Net)
ISBN: 013046130X
EAN: 2147483647
Year: 2002
Pages: 119
Authors: Alan Gordon

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net