GUID Generation and VB6 Binary Compatibility

When exposing managed types as COM types, your classes must have CLSIDs, your interfaces must have IIDs, and so on.  System.Runtime.InteropServices provides a custom attribute (GuidAttribute) that enables you to be explicit about these GUIDs.  But the CLR also has a reasonable algorithm for generating GUIDs on-the-fly, so you normally don't need to be explicit about it.  You can see evidence of this in type libraries you export from assemblies: Although you never marked your managed types with GUIDs, the exported type library shows that every type has one.

The goal of the GUID-generation algorithm is to change GUIDs when incompatible changes are made to managed types, but to keep them unchanged when compatible changes are made.

Here's how it works:

  • CLSIDs (and GUIDs on stucts) are generated based on a hash of the fully-qualified class name plus the name, version, and public key of the assembly containing the class.
  • IIDs are generated based on a hash of the fully-qualified interface name plus the signatures of all the interface s members that are exposed through Interop.
  • LIBIDs are generated based on a hash of the name, version, and public key of the assembly.

This gives you reasonable behavior... as long as you stop VS.NET from giving your project a wildcard version like "1.0.*", which increments the assembly version number each time you recompile.  For example, adding a new method to your class won't change its CLSID, but adding a new method to your interface will change its IID (since interfaces should never change).  Upgrading your assembly version number will change your class's CLSID, but won't change your interface's IID.  And if you don't want any of your GUIDs to change when your assembly version number changes, you can use the ComCompatibleVersionAttribute (introduced in v1.1) to plug in a different version number as input to the GUID-generation algorithm.  In v1.1, several .NET Framework assemblies marked themselves with ComCompatibleVersionAttribute to keep their auto-generated GUIDs matching what was shipped in v1.0.

Let's compare this behavior to Visual Basic 6.  VB6, like managed code, provides an easy way to write COM components.  (Of course, managed code is usually used with other goals in mind!) VB6 hides GUIDs from you when you write COM components, but unlike managed code:

  • The default GUID-generation behavior is not very helpful.  Your types get brand new GUIDs each time you recompile.
  • There is no way to explicitly choose your GUIDs in source code.

However, VB6 does provide an important option for giving your types stable GUIDs: binary compatibility.  The binary compatibility option (on the Properties->Component tab in the IDE) tells the VB6 compiler to look at the type library embedded inside the previously-compiled DLL or EXE, extract the GUIDs, and reuse those GUIDs each time you recompile.  Using this option is almost essential when it comes to managed code interoperating with VB6 COM components.

A simple example that breaks without binary compatibility is the following:

  1. Compile a VB6 COM component that does not use the binary compatibility option.
  2. Import the type library (using TLBIMP.EXE or VS.NET) to create an Interop assembly.
  3. Consume the Interop assembly in managed code.
  4. Recompile the COM component.  Re-running the managed consumer is now broken.

If the COM class was still registered under the old CLSID, instantiating it would work, but you'd get an InvalidCastException as soon as the CLR attempts to call QueryInterface with an IID that the recompiled COM object no longer recognizes.  This happens because the GUIDs from the old type library are captured in the metadata inside the Interop assembly, which is now out of date.  Re-importing the type library would fix this problem, but using binary compatibility prevents you from having to do this each time you recompile.

A more subtle problem from not using binary compatibility is the following situation, which a colleague and I recently encountered out in the field.  A VB.NET client was calling two methods on a VB6 COM component.  The program worked fine when running standalone.  But when the COM component was running inside the VB6 debugger, the first method call succeeded and the second method call threw an exception.

Why did this happen?  The VB.NET code was making late-bound calls to the COM component (since it was declared "As Object") and the second method returned a UDT.  In other words, a VT_RECORD VARIANT was being returned via an IDispatch::Invoke call.  In order for the CLR to map the returned VT_RECORD into a managed type, the managed definition of the structure must be registered under HKEY_CLASSES_ROOT\Record\{GUID}.  (Note that REGASM.EXE does this when you run it on an Interop assembly.)  The program worked outside of the VB6 debugger because at some point in the past, the Interop assembly for the pre-compiled COM component was already registered.  But the VB6 project was not using the binary compatibility option, so the UDT was being assigned a different GUID every time we hit F5 in the debugger!  When the CLR retrieved the GUID for the structure, it found nothing in the registry.  Enabling the binary compatibility option instantly fixed the issue, since VB6 started assigning the structure its GUID from the previously-compiled type library, which was already correctly registered.

So, when programming with VB6, use the binary compatibility option!

Here's a KB article that attempts to clarify VB6's three compatibility options.

And since we're talking so much about GUIDs, here's a stupid parlor trick:

  1. Create a fresh GUID using your favorite means of invoking the CoCreateGuid API (i.e. running GUIDGEN.EXE, UUIDGEN.EXE, calling Guid.NewGuid from managed code, etc.).
  2. Starting with the third cluster of digits and proceeding from left to right, find the first digit between 0-9 (inclusive) and multiply it by its zero-based position in the GUID (using base 10 for all math).  For example, with the GUID 55fe1e41-b221-2d9a-9e61-550737c019ed, I'd take the first digit in the third cluster (2) and multiply it by its position (12) to get the number 24.
  3. Subtract 30 from the resulting number and divide the result by 2.
  4. The number you're left with is 9.  How did I know that?