Wednesday, July 11, 2007

Marshalling Arrays To VB6's COM Funland

Ok, so, please, don't ask why I've found myself writing a COM component in C#, and am using it in VB6. Just accept that fact that I need to.

This COM component needs to return an array of data to VB6. At first I thought "Why not just have my C# project reference the VB6 runtime via interop, and I'll be able to instantiate a VB6 Collection class, and return that to VB6, and it will be happy.

But no, when I try to instantiate a VBA.CollectionClass I get:


Retrieving the COM class factory for component with CLSID {A4C4671C-499F-101B-BB78-00AA00383CBB} failed due to the following error: 80040154.




After much googling, the closest answer I could get was that I was trying to instantiate a VBA.Collection on an x64 platform. Um, the last time I checked my Pentium-D was a 32 bit processor. I even went so far as to force my C# COM component to compile to x86 specifically instead of the 'Any CPU' configuration. Still, the error persisted.

So, I had to bail on that idea, and come up with "Hey, what about just returning an array? What a great idea! I performed a test.

I created a test COM visible interface and class, something like:



[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface ITest {
   String[] GetStrings();
}

[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class Test : ITest {
   public String[] GetStrings() {
      List foo = new List();
      foo.Add("hello");
      foo.Add("bye-bye");

      return foo.ToArray();
   }
}


Then, the corresponding test-code in VB6:


Dim testObject as Test
Dim strings() as String

set testObject = new Test
strings = testObject.GetStrings()



It worked.

I was happy, and I continued working on my C# code as planned.


Days later, I have hundreds of lines of code down in C#, and I'm at a good point to test what I've written. Now, my objects weren't going to be returning string-arrays like the test case above, rather, they're going to be returning arrays of an interface defined in my C# COM component, something like:


[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IBusinessObject {
   Int32 GetSomething();
}

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IFoo {
   IBusinessObject[] GetBusinessObjects();
}



[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class BusinessObject : IBusinessObject {
   /* you get the idea */
}

[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class Foo : IFoo {
   public IBusinessObject[] GetBusinessObjects() {
      List bos = new List();
      /* Add some BusinessObjects to 'bos' */

      return bos.ToArray();
   }
}



Then, the corresponding VB6 code:


Dim foo as Foo
Dim myObjects() as BusinessObject

set foo = new Foo
myObjects = foo.GetBusinessObjects()




Unfortunately, it didn't work. While no exception was thrown in C#, somewhere between return bos.ToArray() and VB6's assignment to the myObjects array a "Type Mismatch" error was thrown in VB6. I couldn't figure out why, though.

I tried catching the array returned from .GetBusinessObjects() into a Variant like:


Dim myObjects as Variant
myObjects = foo.GetBusinessObjects()



I still received the "Type Mismatch" error. I was really lost here, because in VB6-land a Variant is the closest thing you're going to get to a generic object pointer as you're going to get.

I again didn't garner much assistance from another thorough Google spelunking. In my searches, I did stumble across the MarshalAs attribute, but since I'm not a COM-master I wasn't entirely sure what I should be marshaling an array of interfaces as in order to safely reach COM-world. I blindly tried a number of things, and always ended up with "Type Mismatch". I was loosing hope. (As a side note I desperately need to get a copy of Adam Nathan's book .NET and COM: The Complete Interoperability Guide.)

Finally, I stumbled upon it, and I'm not entirely sure why I didn't try it first:

The Answer!

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IFoo {
   [MarshalAs(UnmanagedType.AsAny)]
   IBusinessObject[] GetBusinessObjects();
}



What's weird is that this produces a compiler warning making it sound like the attribute is not of any use:


Type library exporter warning processing 'MyNamespace.IFoo.GetBusinessObjects(#0), MyProject'. Warning: Type contains [MarshalAs(AsAny)], which is only valid for PInvoke. The MarshalAs directive was ignored.



If you place the attribute on the actual implementation you do not get the compiler warning...but you also get the Type Mismatch again. So, it would seem the warning is ignorable as it is applying to something.

Something tells me that once I get my hands on Adam Nathan's book, this will become a lot more obvious to me, and/or I'll find a better answer. Either way, I've got a solution for now.


So, why this post? Mostly as a note-to-self as to how to solve the problem in the future, but there's the hope that some poor sap such as myself has had the exact same problem and that this will turn up for them while they spelunk Google.

If this proves useful to anyone, please, drop me a line.

7 comments:

Anonymous said...

Very useful, but it should be noted that attribute MarshalAs must be on a return declaration:

[return: MarshalAs(UnmanagedType.AsAny)]

Jason Poll said...

Good point, I'll have to update the post at some point...or just be lazy and rely on these comments to tell the tale. :D Thanks!

I've gotten far enough into Adam Nathan's book to realize that marshaling something 'AsAny' is marshaling it as a void-pointer (void *).

Fatboy said...

I stumbled onto your blog entry through a Google search looking for a solution to a similar problem.

I figured out how to best do this and just wanted to post the solution here if anyone else stumbles onto this in the future :)

So your example will work if you just specify how you want your return values marshalled. In your case you only needed to add this to your interface:

public interface IFoo {
[return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_DISPATCH, SafeArrayUserDefinedSubType = typeof(BusinessObject))]
IBusinessObject[] GetBusinessObjects();
}

Then things should have worked just fine :)

Anonymous said...

I think this solves the problem without hastles :)

http://support.microsoft.com/kb/323737

Shah afghan said...

Unexpected error number 28 has occurred: Out of Stack space
this message occure on package & deploment wizard, can you someone help me , solve this problem
Shah Afghan

Shah afghan said...

[Unexpected error number 28 has occurred: Out of Stack space]
can some one help me to solve the mention problem
shah Afghan

Anonymous said...

After two days of googling and fight with C#/VB6 inteop, your post saved my day. Thank you ery much!