Thursday, October 16, 2008

Troubles Marshalling System.Guid to Visual Basic 6.0 (VB6)

I find myself in the <finger-quote>interesting</finger-quote> situation of having to interop a large portion of .NET code back into an older VB6 code base. As it turns out, I need to pass a System.Guid to VB6. Before exposing a property that returned a System.Guid I made sure that System.Guid was COM-Visible - it is:

[Serializable]
[ComVisible(true)]
public struct Guid : IFormattable, IComparable, IComparable<Guid>, IEquatable<Guid> {
   // remainder of metadata snipped for brevity
}

But let's get this point across right from the beginning: System.Guid is marked as being COM-Visible, but it is not usable in VB6! After adding a reference to mscorlib, VB6 will allow you to create a variable of type System.Guid:

Dim myGuid as mscorlib.Guid 

But you are unable to actually make use of 'myGuid' variable.  There is no intellisense pop-up when you type 'myGuid.', there's no way to create one or populate a declared mscorlib.Guid

I'm not entirely sure what the problem is, but I have to assume it has to do with Guid being a struct. Regardless of the reason for the problem, I still needed access to a System.Guid in VB6. Thankfully, all my problem requires is to fetch the Guid from a .NET object, and then pass that Guid to a stored procedure that's expecting a SQL Server "uniqueidentifier" data type.

I started searching. Talk about a tough problem to google for!  There's a lot of really good hits regarding Guid's an creating COM-visible classes/interfaces, but I found very little info on actually marshalling a System.Guid back-n-forth to/from .NET. Thankfully, I was able to find this post where someone is experiencing the exact same problem.  Unfortunately, there was no good answer. Someone did post a reply which didn't answer the original poster's question, but does include a link to an Adam Nathan blog post discussing customizing marshalling, which uses a System.Guid as an example. It's informative, but it doesn't offer any assistance with the problem.

For verbosity (and the morbidly curious,) let's look at a simple example illustrating the problem.  Here's an interface and a class that implements that interface, all exposed to COM. (This is in C#, but can just as easily be written in VB.NET.)

[Guid("BD95337B-0395-4773-B2FC-2BC812388BAB")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IGuidGetter {
   Guid GetSessionID();
}
 
[Guid("6AAFF9A3-875D-4955-AE09-634A88D9EC56")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
[ProgId("JTPApp.GuidGetter")]
public class GuidGetter : IGuidGetter {
   public Guid GetSessionID() {
      return Guid.NewGuid();
   }
}

To use that class in VB6, we compile it, regasm it, and add the registered type lib as a reference to the VB6 project. We can now do something like:

Dim myGuidGetter As New GuidGetter
Dim myGuid As mscorlib.Guid

myGuid = guidGetter.GetSessionID()

Unfortunately, Line 4 will fail to compile, giving you a "Variable uses an automation type not supported in Visual Basic" error message.


Decorating the .NET interface and/or implementation with [MarshalAs(UnmanagedType.LPStruct)], while it does change the COM IDL (as noted and demonstrated in Adam Nathan's article,) doesn't make any difference from a VB6 standpoint. You always get the "Variable uses an automation type not supported in Visual Basic" error.


The really aggravating part for me is that I don't need to really use the Guid in VB6.  I simply need VB6 to retrieve it, and then stuff it into a stored procedure call! 

With this in mind, I tried returning the Guid as an object, but figured it would have problems because I would be boxing a value type in a reference type and then expecting VB6 to know how to properly unbox it. VB6 was able to retrieve the Guid into a Variant, but upon trying to set it as a parameter to the stored procedure, I received an AccessViolationException in my .NET code. Not pretty.

Thankfully, my work-around to the problem turns out to be pretty simple: Return the Guid as a string.  So, to follow through with the example code I started above, if you change it to look like:

[Guid("BD95337B-0395-4773-B2FC-2BC812388BAB")]  
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]  
[ComVisible(true)]  
public interface IGuidGetter {  
   string GetSessionID();  
}  
 
[Guid("6AAFF9A3-875D-4955-AE09-634A88D9EC56")]  
[ClassInterface(ClassInterfaceType.None)]  
[ComVisible(true)]  
[ProgId("JTPApp.GuidGetter")]  
public class GuidGetter : IGuidGetter {  
   public string GetSessionID() {  
      return Guid.NewGuid.ToString();  
   }  
} 

It will work.  You get the Guid, as a string, in VB6.  There's only one minor catch to getting SQL Server to be able to convert that string into a unique identifier, it must be enclosed in curly braces, like so:

  1. Dim sqlCommand As New ADODB.Command
  2. With sqlCommand
  3.    .ActiveConnection = dbConn
  4.    .CommandText = "A_Stored_Procedure"
  5.    .CommandType = adCmdStoredProc
  6.    .Parameters.Refresh
  7.           
  8.    Dim tmp As String
  9.    tmp = g_UserInfo.GetSessionID()
  10.    .Parameters("@sessionID").Value = "{" & tmp & "}"
  11.           
  12.    .Execute , , adExecuteNoRecords
  13. End With
  14. Set sqlCommand = Nothing

On line 10, you can see that I wrapped it in curly-braces.  So while this works for my scenario, there's no way, that I've figured out, to easily interop a System.Guid value back and forth between .NET and VB6.

Anybody have useful insight in how to get the back-n-forth marshalling to work?  I can't imagine I'm the only one who's ever needed to do this.

No comments: