When working with .NET, it is lovely when everything we’re interacting with is rooted in .NET code; however, sometimes we need functionalities that can only be found in one or more COM types.

Something that should cross our minds when dealing with COM types is what the implications may be for deployment efforts. Depending on the specific COM type, its type library may not be a stock component of a Windows installation. If that’s the case (and sometimes, regardless of the case), we should all be prepared for the possibility of the following occurring when instantiating one of the COM types:

COMException: Class not registered (Exception from HRESULT: 0×80040154 (REGDB_E_CLASSNOTREG))

What Not to Do

Don’t let this exception get thrown without handling it (and I mean actually handling it, not just logging its occurrence and re-throwing it or some such).

Nothing is more confusing to your end users than COM-related errors (they tend to evoke enough confusion among developers themselves).

If you attempt to handle it and fail, then you will probably have to bubble up the exception (albeit without the HRESULT part; surely we can think of something more readable). But…at least you tried!

What to Do

First and foremost, and slightly off-topic, you can protect your product from almost ever encountering one of these errors by taking the proper procedures during its deployment. When installing .NET components that are dependent on COM types, your installer should register those COM types, unless for reasons legal or otherwise, you aren’t installing the COM libraries your product is dependent on.

Your installer should not register those COM types by making use of the self-registration capabilities exported by the COM libraries themselves (we will only be tapping into that when handling the above exception at run-time). Rather, we should know beforehand what entries need to be added into the registry, and have our installer manually do that. This is ancient wisdom shared among Those Who Know, and I shall leave it up to the reader to figure out how one goes about doing this.

If your .NET program is making use of one or more COM types, you should prepare for the possibility of the above exception occurring and go about solving it in the most direct manner: by registering the type!

You should do this even if you are protecting yourself during installation as described above, and even if the type originates from a library that comes standard on most machines. I don’t have any details for specific cases on hand, but I’ve seen Windows Update (or some other unknown externality) knock the registration out of some important (though I suppose, not critically important) COM types among end users of one product I deal with.

There’s two ways to go about registering the COM type:

  1. By creating a regsvr32.exe process with arguments supplied pointing it to the COM DLL file
  2. By doing what regsvr32.exe does with our own code

Clearly #2 is the winner here; I’d have to be completely out of other options before ever doing anything that requires the creation of a separate process.

If we’re going to do #2 though, we need to know a bit on how COM registration works. If you’ve ever developed your own COM library, you’re probably quite familiar with the process. But for everyone else, we’ll do a brief overview.

COM Registration in a Nutshell

There are many ways that the registration of COM type libraries occurs, however we will be focusing on a specific aspect of COM registration: self-registration.

Most COM modules exhibit the capability of registering themselves. This is what regsvr32.exe taps into when doing its thing.

A COM module that can self register exports a number of functions, one of them being DllRegisterServer. We need to import this function, and execute it. Therefore, we need to design a .NET type that can do the following:

  1. Load the COM module given a file name or path using LoadLibrary
  2. Import the exported DllRegisterServer function using GetProcAddress
  3. Execute the imported function.
  4. Handle the result returned from doing #3.

The rest of this article details the creation of such a class.

P/Invoke Imports

To accomplish what we’re looking to do, there are a number of unmanaged functions we need to make use of. Being the good developers that we are, we will be adding all these imports to our NativeMethods class.

First off, we will need some way to load the COM libraries, thus we need to import the LoadLibrary function:

/// <summary>
/// Loads the specified module into the address space of the calling process.
/// </summary>
/// <param name="fileName">The name of the module.</param>
/// <returns>If successful, a handle pointing to the loaded module; otherwise, an invalid handle.</returns>
[DllImport("kernel32", CharSet=CharSet.Unicode, SetLastError=true)]
internal static extern LibraryHandle LoadLibrary(string fileName);

 

LoadLibrary in NativeMethods.cs

Conversely, we’ll need a way to unload these modules, thus we need to import the FreeLibrary function:

/// <summary>
/// Frees the loaded dynamic-link library module and, if necessary, decrements its reference count.
/// </summary>
/// <param name="hModule">A handle to the loaded library module.</param>
/// <returns>If successful, a nonzero value; otherwise, zero.</returns>
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[DllImport("kernel32", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FreeLibrary(IntPtr hModule);

FreeLibrary in NativeMethods.cs

Next up, we need to be able to import the exported DllRegisterServer function, thus we need to import the GetProcAddress function:

/// <summary>
/// Retrieves the address of an exported function or variable from the specified dynamic-link library.
/// </summary>
/// <param name="hModule">A handle to the loaded library module.</param>
/// <param name="lpProcName">The function name, variable name, or the function's ordinal value.</param>
/// <returns>If successful, the address of the exported function is returned; otherwise, a null pointer.</returns>
[DllImport("kernel32", CharSet=CharSet.Ansi, ExactSpelling=true, BestFitMapping=false, ThrowOnUnmappableChar=true, SetLastError=true)]
internal static extern IntPtr GetProcAddress(LibraryHandle hModule, [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

GetProcAddress in NativeMethods.cs

You may have noticed a type that does not exist in your environment, LibraryHandle, which brings us to the next part of this article…

Level-0 Type: LibraryHandle

For our objective, we need to define a new level-0 type. If you don’t understand the meaning of the term level-0 type, then I suggest getting acquainted with the following article.

The level-0 type in particular that we need to define is one that will safely keep ahold of our loaded module handles. Also, as we should all know by now, these module handles need to be cleaned up by executing the FreeLibrary function.

The following is the definition of our type (and note, I am not adhering to the standard Microsoft uses where they prefix all these sort of types with the word Safe. Frankly, although it may make sense for Microsoft, I would find it ridiculous to be using it myself):

/// <summary>
/// Provides a level-0 type for library module handles.
/// </summary>
internal sealed class LibraryHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LibraryHandle"/> class.
    /// </summary>
    private LibraryHandle()
        : base(true)
    { }

    /// <inheritdoc/>
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    protected override bool ReleaseHandle()
    {
        return NativeMethods.FreeLibrary(handle);
    }
}

LibraryHandle.cs

Level-1 Type: COMServer

With everything in place, we can now implement our COM registration utility. Note that the following code makes use of some in-house types and extension methods I’ve made; I haven’t the heart to change them, and they should be easy enough to figure out a substitute for:

/// <summary>
/// Provides an in-process server that provides COM interface implementations to clients.
/// </summary>
/// <remarks>
/// <para>
/// This class, as it is currently implemented, mainly handles the registration responsibilities of a COM
/// server, and thusly, in a sense, functions as a level-1 type for COM library handles.
/// </para>
/// <para>
/// This class provides COM server functionalities, it is obviously not an actual COM server (i.e. this
/// class will never handle calls made to <c>QueryInterface</c>).
/// </para>
/// </remarks>
[DisposableObject]
public sealed class COMServer : IDisposable
{
    /// <summary>
    /// Instructs an in-process server to create its registry entries for all classes supported in a server
    /// module.
    /// </summary>
    /// <returns>The result of the operation.</returns>
    internal delegate ResultHandle DllRegisterServer();

    private readonly LibraryHandle _libraryHandle;

    /// <summary>
    /// Initializes a new instance of the <see cref="COMServer"/> class.
    /// </summary>
    /// <param name="libraryHandle">A handle to the COM library to provision.</param>
    private COMServer(LibraryHandle libraryHandle)
    {
        _libraryHandle = libraryHandle;
    }

    /// <summary>
    /// Creates an in-process server that will provide the COM interface implementations found in the
    /// specified COM library to the client.
    /// </summary>
    /// <param name="fileName">The name or path of the COM library module to load.</param>
    /// <returns>
    /// A <see cref="COMServer"/> instance representing a provisioning COM server of the library
    /// specified by <c>fileName</c>.
    /// </returns>
    public static COMServer Create([NotNullOrEmpty]string fileName)
    {
        LibraryHandle libraryHandle = NativeMethods.LoadLibrary(fileName);

        if (libraryHandle.IsInvalid)
        {
            int lastError = Marshal.GetLastWin32Error();

            switch(lastError)
            {
                case (int)ErrorCode.FileNotFound:
                    throw new FileNotFoundException(Strings.COMFileNotFound, fileName);
                case (int)ErrorCode.InvalidName:
                    throw new ArgumentException(Strings.COMInvalidSyntaxFormat.InvariantFormat(fileName),
                                                "fileName");
                case (int)ErrorCode.ModuleNotFound:
                    throw new ArgumentException(Strings.COMModuleNotFoundFormat.InvariantFormat(fileName),
                                                "fileName");
                default:
                    throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        return new COMServer(libraryHandle);
    }

    /// <summary>
    /// Instructs the server to create registry entries for all the classes it has loaded.
    /// </summary>
    public void Register()
    {
        DllRegisterServer registerServer = ImportRegistrationServer();

        ResultHandle hResult = registerServer();

        ValidateRegistration(hResult);
    }

    /// <inheritdoc/>
    public void Dispose()
    {
        if (!_libraryHandle.IsInvalid)
            _libraryHandle.Dispose();
    }

    /// <summary>
    /// Validates that registration succeeded, otherwise an exception is thrown.
    /// </summary>
    /// <param name="hResult">The result of the registration operation.</param>
    private static void ValidateRegistration(ResultHandle hResult)
    {
        if (hResult.Successful())
            return;

        string message;
        switch (hResult)
        {
            case ResultHandle.TypeLibraryRegistrationFailure:
                message = Strings.COMTypeLibRegistration;
                break;
            case ResultHandle.ObjectClassRegistrationFailure:
                message = Strings.COMObjectClassRegistration;
                break;
            default:
                message = Strings.COMInternalErrorFormat.InvariantFormat(hResult);
                break;
        }

        throw new Win32Exception(message, hResult.GetException());
    }

    /// <summary>
    /// Imports the exported registration procedure from the loaded COM library.
    /// </summary>
    /// <returns>
    /// The <see cref="DllRegisterServer"/> registration procedure exported by the loaded COM library.
    /// </returns>
    private DllRegisterServer ImportRegistrationServer()
    {
        IntPtr registrationAddress = NativeMethods.GetProcAddress(_libraryHandle, "DllRegisterServer");

        if (IntPtr.Zero == registrationAddress)
        {
            int lastError = Marshal.GetLastWin32Error();

            switch (lastError)
            {
                case (int)ErrorCode.ProcedureNotFound:
                    throw new InvalidOperationException(Strings.COMNoRegistration);
                default:
                    throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
        }

        return (DllRegisterServer) Marshal.GetDelegateForFunctionPointer(registrationAddress,
                                                                         typeof (DllRegisterServer));
    }
}

COMServer.cs

To make use of our new registration utility, we would do something like the following:

using (COMServer server = COMServer.Create("NameOfCOMLibrary.dll"))
{
    server.Register();
}

Use of COMServer

Enjoy!

Matt Weber

I'm the founder of Bad Echo LLC, which offers consulting services to clients who need an expert in C#, WPF, Outlook, and other advanced .NET related areas. I enjoy well-designed code, independent thought, and the application of rationality in general. You can reach me at matt@badecho.com.

  3 Responses to “A .NET COM Registration Server”

  1. Neat way to do it. Thanks

  2. Mark Malvin

  3. Cool, thank you in advance for all your efforts to share this blog. Regards !

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
   
© 2012-2013 Matt Weber. All Rights Reserved. Terms of Use.