Tuesday, February 7, 2012

The quest to replace a VB6 dll with a C# dll continues

I have a VB6 dll that is being used, via COM, in multiple VB6 apps, COM add-ins, and Outlook Forms. It is being used via both late binding [Set o = CreateObject("a.b")] and early binding [Set o = New a.b] techniques. The idea is to replace the VB6 dll with a C# dll that does exactly the same thing, without touching any of the other applications.

I started by creating a C# dll that implements the same COM interface. But this only got me halfway there. I had two problems:


  1. It worked for late binding, but did not work for early binding. With early binding, the program would use the old VB6 dll (if present) or display the "ActiveX component can't create object" message.


  2. It did not work for a method that had an optional String parameter eg. Public Sub TestMethod(ByVal sArgIn As String, Optional sArgOut As String)



Note that in the years that have passed since this project was started (and stopped, and restarted), I've switched from VS2008 to VS2010, and from .Net 1.1 to .Net 4.0. Some of these problems might have arisen from this change in tools.

Identifying the late binding/early binding problem

Further investigation into COM revealed that when a VB6 dll is registered, it writes the following important entries into the registry (for a VB6 DLL called "MyDll.dll" with an exposed class called "TestClass" which therefore has a ProgId of "MyDll.TestClass"):


HKEY_CLASSES_ROOT\MyDll.TestClass\CLSID
+-- (Default) {ABB83F02-4012-45E4-ADD6-D2E79F45381D}


The "{ABB83...}" string is called a GUID which is basically a unique identifier. Following this in the registry, we get (amongst other things):


HKEY_CLASSES_ROOT\CLSID\{ABB83F02-4012-45E4-ADD6-D2E79F45381D}
+-- InProcServer32
+-- (Default) C:\code\MyDll.dll
+-- TypeLib
+-- (Default) {EACF9A0F-461E-4A36-A195-43ECDE3C5FBA}
+-- VERSION
+-- (Default) 1.1


Following the TypeLib entry (and using the "VERSION" value of "1.1") we get:


HKEY_CLASSES_ROOT\TypeLib\{EACF9A0F-461E-4A36-A195-43ECDE3C5FBA}
+-- 1.1
+-- 0
+-- win32
+-- (Default) C:\code\MyDll


All very boring, BUT the path to my VB6 dll is held in two different places. From what I can tell, late binding uses the first path shown at ...\CLSID\...\InProcServer32, and early binding uses the second path shown at ...\TypeLib\...\win32.

By adding the ProgId attribute to my public .Net class (like [ProgId("MyDll.TestClass")]) and registering it via regasm, my C# dll gets placed into the ...\CLSID\...\InProcServer32 entry. Well actually, because we only added the ProgId to our .Net dll, a new GUID is generated. But because late bound calls are resolved using the "MyDll.TestClass" ProgId, any late bound call to MyDll.TestClass will now use the .Net dll instead of the old VB6 dll.

But the ...\TypeLib\...\win32 registry entry still points to the old VB6 dll (or doesn't exist at all if the VB6 dll has been unregistered via "regsvr32 /u"). So early bound clients will still use the old VB6 dll, or crash.

Resolving the late binding/early binding problem

Resolving this is actually pretty straight-forward. We modify the "AssemblyInfo.cs" file in our .Net project, and insert the TypeLib GUID in there (or replace it with our TypeLib GUID if an entry is already there):


[assembly: Guid("EACF9A0F-461E-4A36-A195-43ECDE3C5FBA")]


Note that this is the TypeLib GUID not the CLSID GUID. I only knew the TypeLib GUID by inspecting the registry. I could also have found this TypeLib GUID by looking into one of the *.VBP project files for a VB6 project that early binds to this dll, where I would have seen:


Reference=*\G{EACF9A0F-461E-4A36-A195-43ECDE3C5FBA}#1.1#0#MyDll.dll#


Just to be neat and tidy, we can force .Net to use the same CLSID GUID as the VB6 dll by adding the "GUID" attribute to the .Net public class:


[ProgId("MyDll.TestClass")]
[ComVisible(true)]
[Guid("ABB83F02-4012-45E4-ADD6-D2E79F45381D")]
public class MyProxyClass : MyDllTlbAssembly.TestClass
{ ...


Next up? Solving the optional String parameter problem!

1 comment:

IndyJoe said...

I have the same issue. A vb6 dll that comes in as a interop into .net. I cant pass a string value.
In vb6 it works as objcOrderSend.initCreate objcOrderSend.addOrder objRsTemp.Fields("pageno"), objRsTemp.Fields("ordmode")
ojcOrderSend.sendOrder

but .net needs
objcOrderSend.initCreate();
objcOrderSend.addOrder(1, 2);
objcOrderSend.sendOrder();

and of course it doesnt work