-
Notifications
You must be signed in to change notification settings - Fork 301
COM Registration
Rubberduck is basically a COM Add-in, we must deal with registering components that should be visible to either the user's code (in the case of Rubberduck's API) or to the Visual Basic Editor (in the case of Rubberduck's UI, such as tool windows or custom menu). In order for Rubberduck to work, those components must have appropriate entries in the registry.
Because COM registration often is perceived to be shrouded in mystery, it is common to form myths such as believing that regasm.exe
is mandatory for registering or that it must be in HKLM hive key, and one consequence is that there is a large amount of cargo cult programming around the COM registration. Neither are true. What does trip people up is that COM, being a very big thing and has several related things (e.g. OLE, Automation, COM+, to name a few), there can be varying requirements to get a working COM component depending on how one intend to use the COM component in question. Because we intend to interop with the VBA editor and to be available to the VBA code, we must meet the requirements imposed by VBE, which is implicitly the Automation. Therefore a minimal COM registration will not be acceptable. Furthermore, it is not possible to use a COM component that isn't a part of a type library. Therefore, we must not only describe the COM component but also the type library in order to be acceptable to Automation clients such as Visual Basic.
A recommended reading on what composes a minimum COM registration and additional things we can add is Larry's series of blog posts.
So the takeaway is that we must have the following entries in those sub-branches of the Software\Classes
tree.
CLSID
= Describes the class ID of a component. This contains all the key data required to activate the component, including the DLL it's located in among other things.
Interface
= Describes the interface that a COM component implements. This seems to be opaque but because we are using .NET COM interop, it actually refers to an universal marshaler, which is why we see same GUID {00020424-0000-0000-C000-000000000046}
as its ProxyStubClsId32
. The COM interop then knows what to do with it and associate with the correct COM class behinds the scene.
TypeLib
= Describes the type library that a COM component may be with. Note that simply having a registry entry pointing to the TLB file is not sufficient. All the COM components' CLSID
and Interface
must in turn contain a TypeLib
that refers back to this key.
Record
= This subtree is not documented (or at least I could not find an official documentation) but it seems to enumerate all enumerations that are COM-visible. All enumerations have their own GUID, and therefore is used to help find the constant members of the enumeration and this key enables the clients such as Visual Basic to locate the enumeration in the given DLL.
<ProgId>
= Provides a registry lookup of CLSID. For example, a registry entry with Rubberduck.AssertClass
will have a subkey CLSID
with the same GUID as found within the CLSID
node. That enables the client such as VBA to locate the actual CLSID with human-friendly name from calls such as CreateObject("Rubberduck.AssertClass")
. This is not required for the COM component to function, but we like being friendly to our users, so we include ProgIds.
Both hive has their Software\Classes
. Therefore the same set of registry entries can be written to either simply by changing the root, without any changes to the key, name and value. Obviously, writing to HKCU is nice as this means no admin privilege is required.
However, most of COM lookups may be actually done via HKCR
, which is basically a merge of HKLM\Software\Classes
and HKCU\Software\Classes
, with the entries in HKCU
taking precedence. That implies that if there is a COM registration in both place with differing contents, the entries in HKCU
will effectively trump those in HKLM
.
Because we need to support both 32-bit and 64-bit VBA hosts, we need to ensure the same registry entries are written to appropriate places. Because all registry entries end up in Software\Classes
, on a 64-bit host, we need to write to Software\Classes\Wow6432Node
to make it accessible to the 32-bit host.
A notable item between the 32-bit registry entries and 64-bit entries should be only in the TypeLib
because they must point to a specific TLB file. With the Rubberduck project, we build for Any CPU
, so we do not need to build both 32-bit and 64-bit versions of the DLL files we generate. However, we must generate both 32-bit TLB file and 64-bit TLB file. For that reason, the TypeLib
entries in each must point to the correct TLB. The entry will have a separate win32
and win64
key which should map to the path of the appropriate TLB file.
A full-featured CLSID subkey for a COM component implemented in .NET that is usable for Automation client would generally have those subkeys:
Generic form:
...\CLSID\{<GUID of the implementing class>}
@="<fully qualified class name>"
Example:
...\CLSID\{69E194DA-43F0-3B33-B105-9B8188A6F040}]
@="Rubberduck.UnitTesting.AssertClass"
The subkey defines the actual CLSID of the COM component. Typically each .NET class will have its own CLSID, and it should be globally unique. This is the key used by COM to uniquely identify a class out of all other classes and thus be able to activate the class in question.
The GUID should correspond to the implementing class's Guid
attribute. In case of the AssertClass
class, we have the attribute [Guid(RubberduckGuid.AssertClassGuid)]
, referencing the RubberGuid
static class to provide the GUID for that class.
The default value should correspond to the class name as found within the .NET namespace.
Generic form:
...\CLSID\{<CLSID>}\Implemented Categories
...\CLSID\{<CLSID>}\Implemented Categories\{<CATID GUID>}
Example:
...\CLSID\{69E194DA-43F0-3B33-B105-9B8188A6F040}\Implemented Categories
...\CLSID\{69E194DA-43F0-3B33-B105-9B8188A6F040}\Implemented Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}
Implemented categories, aka CATID, is not particularly required and has generally host-defined implementations. It is up to the client to make use of the CATID and thus change the behavior. For example, some controls may have certain CATID set to indicate it's a particular type of control to be displayed in toolbox. When performing regasm.exe
, it will generate this key, with the same GUID {62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}
which maps to HKEY_CLASSES_ROOT\Component Categories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}
. There, it indicates it's .NET Category
, so we can know that the class is implemented in the .NET. Visual Basic or any COM consumers probably won't care about that but it is useful to the .NET to avoid re-importing COM metatype, so it's recommended to keep the key.
Generic form:
...\CLSID\{<CLSID>}\InprocServer32
@="<DLL name>"
"ThreadingModel"="<ThreadingModel>"
"Class"="<Fully qualified class name>"
"Assembly"="<Assembly name>, Version=<Assembly version>, Culture=<Assembly culture>, PublicKeyToken=<Assembly public key>"
"RuntimeVersion"="<CLR runtime>"
"CodeBase"="<Path to the DLL containing the class>"
Example:
...\CLSID\{69E194DA-43F0-3B33-B105-9B8188A6F040}\InprocServer32
@="mscoree.dll"
"ThreadingModel"="Both"
"Class"="Rubberduck.UnitTesting.AssertClass"
"Assembly"="Rubberduck, Version=2.1.6642.37961, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"="C:\GitHub\Rubberduck\Rubberduck.Deployment\bin\Debug\Rubberduck.dll"
The InprocServer32
describes how the class component may be activated and where it is located on the computer. There are also some .NET specific data included. Normally, a C++ COM registration might have the default value pointing to the its own DLL. However, because we are doing COM interop, we need to point to the mscoree.dll
instead, and additionally pass some of the data to be used by .NET for activating the COM-visible object. This is why we have 4 additional attributes whereas COM itself technically requires only the default value (the DLL name) and the ThreadingModel
. Those 4 additional attributes are then used to locate the class within the .NET assembly, and use the correct CLR runtime (keep in mind CLR version is different from .NET framework).
Note that there is no such thing as InProcServer64
; on 64-bit systems, we use the registry virtualization to distinguish between 32-bit and 64-bit version of class which in the case of COM interop is somehow irrelevant because we use Any CPU
so the Assembly
value will remain the same in both 64-bit and 32-bit path. There is a InProcServer
which is used for 16-bit process and is not used by us.
Generic form:
...\CLSID\{<CLSID>}\InprocServer32\<Version>
<same keys as non-version-specific InProcServer32>
Example:
...\CLSID\{69E194DA-43F0-3B33-B105-9B8188A6F040}\InprocServer32\2.1.6642.37961
"Class"="Rubberduck.UnitTesting.AssertClass"
"Assembly"="Rubberduck, Version=2.1.6642.37961, Culture=neutral, PublicKeyToken=null"
"RuntimeVersion"="v4.0.30319"
"CodeBase"="C:\GitHub\Rubberduck\Rubberduck.Deployment\bin\Debug\Rubberduck.dll"
We can additionally describe version specific behavior and thus customize accordingly. In practice, however, only one is actually used and there shouldn't be multiple versions active at same time anyway. Thus, there is generally only one version subkey. Note that the default value is not set, since it is already declared in the non-specific InProcServer32
subkey. We can only customize the class, assembly, runtime version and the path to the file, but at this time, they will be all the same.
Generic form:
...\CLSID\{<CLSID>}\ProgId
@="<ProgId>"
Example:
...\CLSID\{8D052AD8-BBD2-4C59-8DEC-F697CA1F8A66}\ProgId
@="Rubberduck.AssertClass"
This is required whenever providing a ProgId
. In this case, this correspond to the ProgId
attribute which the class has. We use RubberduckProgIds
static class to map all those.
Though the CLSID
is the lynchpin of COM component, we always access it via interfaces and a coclass may in fact implement several interfaces, some documented, some not documented, There is no mechanism for enumerating them except what is already reported via the type library and the registry. Therefore, when we implicitly make a IUnknown::QueryInterface
, we will be looking up the interface ID (aka IID
), and this is where the branch becomes relevant; it describes the interfaces.
Generic form:
...\Interface\{<GUID>}
@="<Interface name>"
Example:
...\Interface\{69E194DB-43F0-3B33-B105-9B8188A6F040}
@="IAssert"
This key identifies the actual interface. This should map to an actual interface, which in this case is the [`IAssert` interface](https://github.com/rubberduck-vba/Rubberduck/blob/next/Rubberduck.Main/ComClientLibrary/Abstract/UnitTesting/IAssert.cs). This interface is used by the `AssertClass` we saw earlier because we have the [attribute `ComDefaultInterface`](https://github.com/rubberduck-vba/Rubberduck/blob/next/Rubberduck.Main/ComClientLibrary/UnitTesting/AssertClass.cs#L13) set. Note that the attribute is not strictly required; simply implementing the interface is sufficient though the attribute is useful when a class implements more than one COM-visible interface so you can explicitly specify which is to be its default interface.
Furthermore, the GUID corresponds to the interfae's ID (`IID`) and is declared similarly to how we declare CLSID as illustrated in [`IAssert` interface](https://github.com/rubberduck-vba/Rubberduck/blob/next/Rubberduck.Main/ComClientLibrary/Abstract/UnitTesting/IAssert.cs#L11), which also maps back to the [`RubberduckGuid` class](https://github.com/rubberduck-vba/Rubberduck/blob/next/Rubberduck.Core/RubberduckGuid.cs#L20).
### **ProxyStubClsid32**
Generic form:
...\Interface{}\ProxyStubClsid32 @="{00020424-0000-0000-C000-000000000046}"
Example:
...\Interface{69E194DB-43F0-3B33-B105-9B8188A6F040}\ProxyStubClsid32 @="{00020424-0000-0000-C000-000000000046}"
Similar to the `CLSID`, we need to describe how to "activate" an interface and for that we can use the universal marshaler that is also used by classes. However, by itself, that is not enough; the universal marshaler depends on having a type library, since we aren't providing our own proxy DLL. For that reason, the next key is vital.
### **TypeLib**
Generic form:
...\Interface{}\TypeLib @="{}" "Version"=""
Example:
...\Interface{69E194DB-43F0-3B33-B105-9B8188A6F040}\TypeLib @="{E07C841C-14B4-4890-83E9-8C80B06DD59D}" "Version"="2.1"
The universal marshaler relies on typelib and the key entry provides it with information it needs to look up the type library to be able to marshal the `IID`. Refer to `TypeLib` section for more information.
rubberduckvba.com
© 2014-2025 Rubberduck project contributors
- Contributing
- Build process
- Version bump
- Architecture Overview
- IoC Container
- Parser State
- The Parsing Process
- How to view parse tree
- UI Design Guidelines
- Strategies for managing COM object lifetime and release
- COM Registration
- Internal Codebase Analysis
- Projects & Workflow
- Adding other Host Applications
- Inspections XML-Doc
-
VBE Events