Turn a simple C# DLL into a COM interop component
How do I make a C# DLL into a COM interop DLL that can be consumed by a VB6 application?
This is the answer I wanted to find in StackOverflow but couldn't. It turns out to be fairly easy to turn a simple C# dll into a COM dll.
To create the C# dll
Create a solution with a C# class project. The class should have an interface for the properties/methods and an interface for the events. Assign GUID attributes to the class and interfaces as described in MSDN - Example COM Class (C# Programming Guide). Also see: MSDN - How to: Raise Events Handled by a COM Sink.
In Project Properties > Application tab > Assembly Information button > check "Make assembly COM-Visible". This makes all public methods in the class COM visible.
In Project Properties > Build tab > Set "Platform target" to x86.
That's all you need to do to create the DLL. To call the DLL, you need to register it.
Registering the DLL on your development machine
You can register the DLL one of these ways:
- Check Project Properties > Build Tab > "Register for COM Interop". This will automatically register the DLL when you build it.
-
Manually register the DLL with RegAsm. This allows you to register the DLL in the directory of your choice, rather than in the build directory. This is the method I used.
- Do not check Project Properties > Build Tab > "Register for COM Interop"
- Copy the DLL to the directory where you want to register it
-
Open a command shell with administrator rights and type
RegAsm.exe -tlb -codebase mydll.dll
RegAsm.exe can be found in "C:\Windows\Microsoft.NET\Framework\v2.0.50727", while "mydll.dll" is the name of your DLL;
tlb
means "create a type library";codebase
means "write the directory location to the Registry, assuming it is not being placed in the GAC".RegAsm will display a warning that the assembly should be strong-named. You can ignore it.
At this point, you should be able to add a reference to the COM DLL in VB6, see it with Intellisense, and run it just like a regular COM DLL.
Installing the DLL with InstallShield
If you are using InstallShield to install the DLL along with the rest of your application, do the following.
In InstallShield, add a new Component to the Components list. Remember to associate the Component with a Feature. Set component property ".NET COM Interop" to Yes.
Add the .dll file to the Files section of the Component. Do not check the "Self-Register" property. Right-click on the .dll file and select "Set Key File".
Add the .tlb file to the Files section of the Component. Check the "Self-Register" property.
The correct version of the .Net Framework needs to exist on the target PC.
That's it.
As en extension to @Kieren Johnstone's answer a practical code example on class modifications you need to do:
From:
public class ApiCaller
{
public DellAsset GetDellAsset(string serviceTag, string apiKey)
{
....
}
}
public class DellAsset
{
public string CountryLookupCode { get; set; }
public string CustomerNumber { get; set; }
public bool IsDuplicate { get; set; }
public string ItemClassCode { get; set; }
public string LocalChannel { get; set; }
public string MachineDescription { get; set; }
public string OrderNumber { get; set; }
public string ParentServiceTag { get; set; }
public string ServiceTag { get; set; }
public string ShipDate { get; set; }
}
To:
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
[ComVisible(true)]
public interface IComClassApiCaller
{
IComClassDellAsset GetDellAsset(string serviceTag, string apiKey);
}
[Guid("7BD20046-DF8C-44A6-8F6B-687FAA26FA71"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IComClassApiCallerEvents
{
}
[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(IComClassApiCallerEvents))]
[ComVisible(true)]
[ProgId("ProgId.ApiCaller")]
public class ApiCaller : IComClassApiCaller {
public IComClassDellAsset GetDellAsset(string serviceTag, string apiKey)
{
.....
}
}
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83E")]
[ComVisible(true)]
public interface IComClassDellAsset
{
string CountryLookupCode { get; set; }
string CustomerNumber { get; set; }
bool IsDuplicate { get; set; }
string ItemClassCode { get; set; }
string LocalChannel { get; set; }
string MachineDescription { get; set; }
string OrderNumber { get; set; }
string ParentServiceTag { get; set; }
string ServiceTag { get; set; }
string ShipDate { get; set; }
}
[Guid("7BD20046-DF8C-44A6-8F6B-687FAA26FA70"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[ComVisible(true)]
public interface IComClassDellAssetEvents
{
}
[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F937"),
ClassInterface(ClassInterfaceType.None),
ComSourceInterfaces(typeof(IComClassDellAssetEvents))]
[ComVisible(true)]
[ProgId("ProgId.DellAsset")]
public class DellAsset : IComClassDellAsset
{
public string CountryLookupCode { get; set; }
public string CustomerNumber { get; set; }
public bool IsDuplicate { get; set; }
public string ItemClassCode { get; set; }
public string LocalChannel { get; set; }
public string MachineDescription { get; set; }
public string OrderNumber { get; set; }
public string ParentServiceTag { get; set; }
public string ServiceTag { get; set; }
public string ShipDate { get; set; }
}
Hope this saves you some time
Most examples in the internet of COM servers contain only one CoClass, and it is claimed that this CoClass must have a public constructor. This is true in this case, but normal servers have more than one CoClass, of which only one can be created, whereas the instances of the non-creatable CoClasses are properties of the creatable CoClass. For example, consider the Word object model with the creatable CoClass Application
that has the Documents
property which in turn consists of instances of the CoClass Document
. The following server has two CoClasses, one with a public constructor and one with a private constructor.
Create a solution for a C# Class Library (.Net Framework), not Class Library (.Net Standard) and name it for example BankServerCSharp. Choose this name wisely, because it will be the main part of the ProgIDs of your CoClasses and the namespace name in C++. This name will also be listed in the References dialog box of C# and VBA.
-
Delete the boilerplate code and add two files Bank.cs and Account.cs. Insert the following code:
//Account.cs using System.Runtime.InteropServices; namespace BankServerCSharp { [ComVisible(true)] // This is mandatory. [InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface IAccount { double Balance { get; } // A property void Deposit(double b); // A method } [ComVisible(true)] // This is mandatory. [ClassInterface(ClassInterfaceType.None)] public class Account:IAccount { private double mBalance = 0; private Account() { } // private constructor, coclass noncreatable public static Account MakeAccount() { return new Account(); } //MakeAccount is not exposed to COM, but can be used by other classes public double Balance { get { return mBalance; } } public void Deposit(double b) { mBalance += b; } } } //Bank.cs using System.Runtime.InteropServices; namespace BankServerCSharp { [ComVisible(true)] // This is mandatory. [InterfaceType(ComInterfaceType.InterfaceIsDual)] public interface IBank { string BankName { get; set; } // A property IAccount FirstAccount { get; } // Another one of type IDispatch } [ComVisible(true)] // This is mandatory. [ClassInterface(ClassInterfaceType.None)] public class Bank:IBank { private string Name = ""; private readonly Account First; public Bank() { First = Account.MakeAccount(); } public string BankName { get { return Name; } set { Name= value; } } public IAccount FirstAccount { get { return First; } } } }
Build the project with the configuration Release/Any CPU. The output is the managed DLL BankServerCSharp.dll located in the \bin\release folder.
Now you must register your managed COM DLL. Don't try regsvr32, there is a special program called regasm for managed COM DLLs. Regasm has a version for 32-bit and for 64-bit apps. Open a command prompt as administrator and change to C:\Windows\Microsoft.NET\Framework\v4.0.30319. This folder contains the regasm.exe app to register the managed COM DLL as if it would be a native 32-bit COM DLL.
Type
RegAsm.exe /tlb /codebase path_to_your_bin_release_folder\BankServerCSharp.dll
. You must register your DLL on any computer in this way. Don’t forget the /tlb switch that creates the type library. The compiler will comment the switch /codebase with some warnings that you can ignore. The DLL is registered in the WoW64 part of the registry and can be used by native (unmanaged) 32-bit apps.Now repeat the registration for usage of the managed COM DLL by 64-bit apps. Change to C:\Windows\Microsoft.NET\Framework64\v4.0.30319 and type the same command as before.
-
You can speed up the registration on your own PC by running Visual Studio with administrative rights and adding the following post-build events:
%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /tlb /codebase "$(TargetPath)" %SystemRoot%\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe /tlb /codebase "$(TargetPath)"
You can now use your DLL like a native unmanaged COM DLL. Test your DLL with VBA: Under Tools/References tick BankServerCSharp. If it is not shown, the registration failed. A simple test sub:
Sub TestSOExampleNew()
On Error GoTo Oops
Dim BiBiBaBa As New BankServerCSharp.Bank 'New!
BiBiBaBa.BankName = "Big Bird Bad Bank"
Dim Account As BankServerCSharp.Account 'No New!
Set Account = BiBiBaBa.FirstAccount
Account.Deposit 2000
MsgBox BiBiBaBa.BankName & ". First client's balance: " & Account.Balance
Exit Sub
Oops:
MsgBox "Sorry, an unexpected error occurred!"
End Sub
To test your managed COM DLL in C++, create a new Console Application, insert the following code and build as Release/x64 or Release/x86:
#include "stdafx.h"
#import "D:\Aktuell\CSharpProjects\BankServerCSharp\BankServerCSharp\bin\Release\BankServerCSharp.tlb"
//this is the path of my C# project's bin\Release folder
inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_error(x); };
int main()
{
try
{
TESTHR(CoInitialize(0));
BankServerCSharp::IBankPtr BankPtr = nullptr;
TESTHR(BankPtr.CreateInstance("BankServerCSharp.Bank"));
BankPtr->BankName = L"Ernie First Global Bank";
BankServerCSharp::IAccountPtr AccountPtr = BankPtr->FirstAccount;
TESTHR(AccountPtr->Deposit(200.09));
wprintf(L"Name: %s, Balance: %.2f\n", (LPCWSTR)BankPtr->BankName, AccountPtr->Balance);
}
catch (const _com_error& e)
{
CStringW out;
out.Format(L"Exception occurred. HR = %lx, error = %s", e.Error(), e.ErrorMessage());
MessageBoxW(NULL, out, L"Error", MB_OK);
}
CoUninitialize();// Uninitialize COM
return 0;
}