Search code examples
c#.netcomvb6interop

Using ,NET Generic List ToArray in a COM Called Wrapper causes access violation, am I missing something?


I'm having a problem when trying to pass an array back to a COM caller.

Basically, I have a generic List of classes, that I want to return to the COM caller, you can't use generics in COM, so it's got to be strongly typed. I figured just returning a .ToArray() on the List would give me what I need.

When I use it in VB6, it works perfect. In the debugger. When I compile the app and run it as just a straight executable, it crashes with a memory access violation. I'm pretty stumped. I'm unable to trap any errors in VB or in the .NET component, so I'm guessing something is getting munged in the COM translation.

It fails on "return a" in getList()

Application Event Log contains this error:

Faulting application project1.exe, version 1.0.0.0, stamp 49fb60d8, faulting module msvbvm60.dll, version 6.0.98.2, stamp 4802a186, debug? 0, fault address 0x00106154.

Here's the VB6 code for reference, it's stupid simple.

Private Sub Form_Load()
  Dim c1 As IClass1
  Dim c2

  Set c1 = New Class1

  For Each c2 In c1.getList
    Debug.Print c2.Value
  Next

End Sub

And the .NET code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace TestCOM
{

    public interface IClass1
    {
        List<IClass2> MyList { get; }

        IClass2[] getList();
    }

    public class Class1 : IClass1
    {
        public List<IClass2> MyList { get; private set; }

        public Class1()
        {
            this.MyList = new List<IClass2>();
            this.MyList.Add(new Class2());
            this.MyList.Add(new Class2());
        }

        public IClass2[] getList()
        {
            try
            {
                System.IO.File.WriteAllText(@"C:\COMLog1.txt", "Hi.");
                IClass2[] a = new IClass2[this.MyList.Count];
                System.IO.File.WriteAllText(@"C:\COMLog1.txt", "Bye.");
                for (int i = 0; i < this.MyList.Count; i++)
                {
                    a[i] = this.MyList[i];
                }
                System.IO.File.WriteAllText(@"C:\COMLog1.txt", "Sup.");

//The logging appears on disk all the way to here, so the failure appears to be in the interop layer.

                return a;
            }
            catch (Exception e)
            {
                System.IO.File.WriteAllText(@"C:\COMLog.txt", string.Format("Error:\n{0}", e.Message));
            }
            return null;
        }
    }

    public interface IClass2
    {
        int Value { get; }
    }
    public class Class2: IClass2
    {
        public int Value { get; private set; }

        public Class2()
        {
            Random r = new Random();
            this.Value = r.Next();
        }
    }
}

The problem stems from late binding on the VB side. Using an early bound array solves the problem, here's the final VB code for the test application:

Private Sub Form_Load()
  Dim c1 As IClass1
  Dim c2() As IClass2
  Dim x

  Set c1 = New Class1

  c2 = c1.getList

  For Each x In c2
    Debug.Print x.Value
  Next

End Sub

Solution

  • It appears that the problem may be with late and early binding. If you declare c2 as a type it should resolve this error. Here is another site which goes over a similar problem.

    http://weblogs.asp.net/psteele/archive/2007/02/06/com-interop-does-not-like-uninitialized-arrays.aspx

    As for being unable to capture the error, it looks like the .ToArray function for generic lists is capable of corrupting the data. Here is some code from the reflector:

    //You can see that the function calls Array.Copy
    Public Function ToArray() As T()
        Dim destinationArray As T() = New T(Me._size  - 1) {}
        Array.Copy(Me._items, 0, destinationArray, 0, Me._size)
        Return destinationArray
    End Function
    
    //You can see from the ReliabilityContract that this method is set to
    //Consistency.MayCorruptInstance and Cer.MayFail
    <ReliabilityContract(Consistency.MayCorruptInstance, Cer.MayFail)> _
    Public Shared Sub Copy(ByVal sourceArray As Array, _
        ByVal sourceIndex As Integer, ByVal destinationArray As Array, _
        ByVal destinationIndex As Integer, ByVal length As Integer)
            Array.Copy(sourceArray, sourceIndex, destinationArray, _
            destinationIndex, length, False)
    End Sub
    

    Additionally, here is some documentation which may help explain why you can't properly trap the error. Basically, the entire AppDomain could become corrupted or there could be a ThreadAbort or a StackOverflow or some other critical error which causes all processing to be aborted. The second link talks about how to resolve these types of problems.