//
you're reading...
.NET/COM/ActiveX Interop, COM

Creating a COM Server Using C#.

1. Introduction.

1.1 COM remains a popular technology today and, with tremendous support from managed language compilers, e.g Visual C#, building COM servers in managed code is a viable option.

1.2 This blog is intended for the C# developer who wants to develop COM servers using C#, either to provide managed implementations for some COM-based framework or simply to expose managed functionality through COM-interop.

1.3 I shall expound on the basic coding requirements, touch on the important registration process and provide sample code.

1.4 The language that I shall use to develop the managed class is C#. The client application will be written in C++.

2. Basic Coding Requirements.

2.1 At the time of this writing, I personally am not familiar with any built-in compiler support nor System.Runtime.InteropServices code support for developing managed COM out-of-proc (exe) servers. All compiler and code support are for building COM in-proc servers (i.e. DLLs).

2.2 I shall thus expound only on building COM DLL servers using a C# project targeted as a class library.

2.3 The following is a summary of the basic coding requirements :

  • The C# class to be exposed as a COM coclass would have to be declared as public in scope.
  • It also must be decorated with the ComVisibleAttribute (with a parameter value of true). This is mandatory.
  • The class must at least expose a public default constructor. This is also mandatory.
  • Other attributes which are useful include the GuidAttribute, ProgIdAttribute and the ClassInterfaceAttribute.
  • Properties and methods of the C# class which are to be callable by unmanaged client applications would have to be declared as public.
  • Although not entirely necessary, the C# class should inherit from a predefined, COM-visible public interface and must hence implement the interface methods.

2.4 For demonstration purposes, I have defined the following C# public interface and class targeted for exposure to COM :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace CSCOMServer
{
    [ComVisible(true)]  // This is mandatory.
    [Guid("4945B34B-1B63-4a58-B5FE-9627FEFAEA9D")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    public interface IInterface01
    {
        void Method01();

        string string_property
        {
            get;
            set;
        }
    }

    [ComVisible(true)]  // This is mandatory.
    [Guid("36E6BC94-308C-4952-84E6-109041990EF7")]
    [ProgId("CSCOMServer.CSCOMClass01")]
    [ClassInterface(ClassInterfaceType.None)]
    public class CSCOMClass01 : IInterface01
    {
        // A default public constructor is also mandatory.
        public CSCOMClass01()
        {
        }

        ~CSCOMClass01()
        {
        }

        public void Method01()
        {
            Console.WriteLine("Method01().");
        }

        public string string_property
        {
            get
            {
                return m_string_property;
            }

            set
            {
                m_string_property = value;
            }
        }

        private string m_string_property;
    }
}

The above code is part of a C# class library project. After compilation, the DLL CSCOMServer.dll will be produced.

2.5 The interface and the implementation class must both be declared as public and be marked with the ComVisibleAttribute. Without being declared as such, they will not be registered into the registry (section 3) and the type library generated for the class library will not contain their information (section 4).

2.6 Here are some notes concerning the importance of the public default constructor :

  • The public constructor must either be defined (as above) or it is left undefined with no other constructors defined in which case a default one will be generated by the compiler.
  • If a default constructor is defined but is marked private then such a class cannot be created at all :
private CSCOMClass01()
{
  ...
}
  •  If no default constructor is defined and non-default constructors (i.e. constructors with parameters) are also defined, the compiler will assume that no default constructors is to be defined for the class, e.g. :
public class CSCOMClass01 : IInterface01
{
  public CSCOMClass01(string str)
  {
    ...
  }

  ~CSCOMClass01()
  {
    ...
  }

  ...
}
  • The main issue about public default constructors is that : If no public default constructor is available for the class, then when a client application attempts to instantiate the class via COM, the COM error 0x80040154 (class not registered) will be returned.

2.7 As mentioned in the last bullet point in 2.3, a COM-visible public interface is not entirely necessary but I believe it is a good practice to define one and then have a C# class implement it. This will allow the interface to be implemented by various managed classes and even in unmanaged code.

3. The Registration Process.

3.1 After successful compilation, the class library DLL would have to be registered for COM by using REGASM.EXE. You must use the /tlb flag which causes REGASM.EXE to produce an associated COM type library.

3.2 The following is a sample call to REGASM.EXE :

REGASM.EXE CSCOMServer.dll /tlb

With the usage of the /tlb flag, a CSCOMServer.tlb type library file will be produced. This type library file is very important for unmanaged client code usage.

3.3 REGASM.EXE essentially records COM-related information about the public and Com-Visible items in the class library (e.g. the CSCOMClass01 class) into the registry for use by a client app.

3.4 Client apps would have to reference this type library in order to instantiate the CSCOMClass01 class.

3.5 Note that once created, the COM object, which is actually a C# object, operate in unmanaged code via a COM-callable-wrapper.

3.6 The registration process will insert the following information into the registry :

From this registration information, we see that the default string value for the “InprocServer32” key for the COM class with GUID {36E6BC94-308C-4952-84E6-109041990EF7} (i.e. the GUID for CSCOMServer.CSCOMClass01) is set to the unintuitive value of “mscoree.dll”.

3.7 This is the Microsoft .NET Runtime Execution Engine. Why is this DLL set as the in-proc server for the CSCOMServer.CSCOMClass01 class ? In actual fact, mscoree.dll acts as the “go-between” between a COM client application and the .NET CLR which performs the loading of the CSCOMServer.CSCOMClass01 .NET class object.

3.8 This is because C# class instances cannot be used directly by an unmanaged client application. The mscoree.dll, an unmanaged DLL, must intercept and produce the appropriate COM-Callable-Wrapper (CCW) for the .NET class for use by the unmanaged client.

4. The Type Library.

4.1 As part of the process of assembly registration, the type library CSCOMServer.tlb is generated. This type library is a very important file in that it contains the COM types associated with the CSCOMServer.dll class library.

4.2 We may use OLEVIEW.EXE to tanslate its contents into IDL :

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: CSCOMServer.tlb

[
  uuid(BBD9D740-74D4-44A5-823E-94472F910294),
  version(1.0)
]
library CSCOMServer
{
    // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
    importlib("mscorlib.tlb");
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface IInterface01;

    [
      odl,
      uuid(4945B34B-1B63-4A58-B5FE-9627FEFAEA9D),
      version(1.0),
      dual,
      oleautomation,
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "CSCOMServer.IInterface01")    

    ]
    interface IInterface01 : IDispatch {
        [id(0x60020000)]
        HRESULT Method01();
        [id(0x60020001), propget]
        HRESULT string_property([out, retval] BSTR* pRetVal);
        [id(0x60020001), propput]
        HRESULT string_property([in] BSTR pRetVal);
    };

    [
      uuid(36E6BC94-308C-4952-84E6-109041990EF7),
      version(1.0),
        custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "CSCOMServer.CSCOMClass01")
    ]
    coclass CSCOMClass01 {
        interface _Object;
        [default] interface IInterface01;
    };
};

4.3 Here are the pertinent points about the IDL code above :

  • The IInterface01 interface is defined with all the properties and methods that we defined in the C# code.
  • Its GUID that matches that which we defined using the GuidAttribute.
  • A coclass named CSCOMClass01 is defined. This represents a COM class which is instantiated by a client app.
  • Its default interface is IInterface01. This is directly so to the fact that we have specified the “[ClassInterface(ClassInterfaceType.None)]” attribute to the corresponding CSCOMClass01 C# class and that we have made the class inherit from IInterface01.
  • The GUID of the coclass also matches that which we have specified using the GuidAttribute.

5. Sample VC++ Client Code.

 5.1 The following is a listing of a sample VC++ client code :

// CPPConsoleClient01.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#import "CSCOMServer.tlb" raw_interfaces_only no_implementation
using namespace CSCOMServer;

int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(NULL);

	if (1)
	{
		IInterface01Ptr spIInterface01 = NULL;

		spIInterface01.CreateInstance(__uuidof(CSCOMClass01));

		if (spIInterface01 == NULL)
		{
			::CoUninitialize();
			return 0;
		}

		BSTR bstr_put = ::SysAllocString(L"Hello World");

		if (bstr_put == NULL)
		{
			::CoUninitialize();
			return 0;
		}		

		spIInterface01 -> put_string_property(bstr_put);

		BSTR bstr_get = NULL;

		spIInterface01 -> get_string_property(&bstr_get);

		spIInterface01 -> Method01();

		::SysFreeString(bstr_put);
		bstr_put = NULL;

		::SysFreeString(bstr_get);
		bstr_get = NULL;
	}

	::CoUninitialize();

	return 0;
}

The above code is a console-based application. The following is a summary of the pertinent points :

  • It imports of the type library of the CSCOMServer server. Doing so will cause the VC++ compiler to interpret the type library and produce code constructs in the C++ language that matches the IDL items.
  • The creation of the coclass CSCOMClass01 is done via the smart pointer IInterface01Ptr.
  • All property access and method invokation are done via an instance of this smart pointer.

6. Locating the C# Class Library at Runtime.

6.1 Newbies often face the COM error 0x80040154 (class not registered) when attempting to create a COM coclass derived from a managed class.

6.2 As long as the managed class has been duely represented in the type library together with associated interfaces, the coclass should be createable.

6.3 The error is often due to the fact that the .NET-based COM server cannot be located by the CLR at runtime.

6.4 Generally speaking, one must be familiar with the way the runtime searches for assemblies (refer to “How the Runtime Locates Assemblies” (http://msdn.microsoft.com/en-us/library/yx7xezcf(v=vs.71).aspx)).

6.5 Flash back to poiny 3.6 where we briefly explored the “InprocServer32” key for the CSCOMServer.CSCOMClass01 COM class. Recall that the default string value for this key is “mscoree.dll”. We have already learnt that at runtime, it is mscoree.dll which is called upon to instantiate the managed class and then create a CCW for it to return to the client.

6.6 Now how does mscoree.dll load the managed class ? Well, it will use the other string values of the InprocServer32 key (especially the “Assembly” and “Class” string values) to internally load the appropriate assembly and thereafter the appropriate class to instantiate.

6.7 The string value for “Assembly” contains the complete identity of the assembly to load. On my machine, this is set to :

"CSCOMServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null".

Note that no path information is written, only the name of the assembly. Hence, at runtime, the usual rules for locating assemblies will apply.

6.8 The string value for “Class” indicates the name of the managed class to instantiate. This is unsurprisingly set to “CSCOMServer.CSCOMClass01”.

6.9 If difficulty in assembly location persists, one of two techniques which are described in the following points may be used.

6.10 Use the /codebase flag when calling REGASM.EXE :

  • When performing registration, add the /codebase flag :
Regasm.exe myDll.dll /tlb:myDll.tlb /codebase
  • The /codebase flag will add an additional CodeBase setting to the registry for the managed class. This CodeBase registry setting contains the full path to the class library DLL.
  • Once this is done, revisit the registry settings for the COM class :

  • Notice that the CodeBase string value contains the full path to the DLL (highlighted in red box). This enables the runtime to directly locate the DLL.
  • Another way to achieve the same end is to set the “Register For COM Interop” option in the class library’s project properties :

  • This is logically equivalent to manually calling REGASM.EXE with the /codebase flag.
  • However, note that the /codebase flag is advocated for development purposes only (see the section on /codebase in “Assembly Registration Tool (Regasm.exe)”http://msdn.microsoft.com/en-us/library/tzat5yw6(v=vs.71).aspx). Hence this technique should only be used to confirm the underlying cause of a 0x80040154 (class not registered) problem.
  • It is more important to understand the runtime assembly search process as mentioned in point 6.4 above.

6.11 The other technique is to copy all registered DLLs (and their dependencies) to the same folder as the application itself. This is another way that the runtime locates assemblies.

7. In Conclusion.

7.1 This blog serves as a primer for creating COM servers out of a managed class library. I hope the reader has benefitted from the sample codes and the various techniques of registration.

7.2 I have focused on the practical problems that newbies tend to face when making initial attempts in building and using managed COM servers : coding requirements, registration and class library location.

7.3 As part of my continuing effort to help the reader further understand COM servers created out of managed code, I have provided additional supplementary tips, useful programming techniques and solutions to various practical problems that have been encountered, etc all of which are relevant to this knowledge domain. Please refer to the following articles :

 

 

 

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

9 thoughts on “Creating a COM Server Using C#.

  1. Thanks so much for your help, Bio … this example was worth its weight in GOLD. It worked as advertised, and I learned a lot … Indeed, I learned enough to get my project back on track. Thank you so much for your help.

    Steve Jensen

    Posted by Steve Jensen | October 24, 2012, 7:49 pm
  2. I’m just trying out your sample, as it’s just what I’ve been attempting to do, but after the spIInterface01.CreateInstance call, spIInterface01 == NULL, and it exits early. Do you know why this may be happening and what I can try to fix this? Thanks!

    Posted by Jeff | February 2, 2013, 12:43 am
    • Hello Jeff,

      1. One possibility (and a common cause) is the fact that the assembly could not be located at runtime. Please read section 6 where this is explained.

      2. If this possibility is ruled out, step into the spIInterface01.CreateInstance() call and check out the return value of CoCreateInstance(). What is the HRESULT ?

      – Bio.

      Posted by Lim Bio Liong | February 20, 2013, 9:19 am
  3. I have an issue about C# client to call C# COM server.
    I know to call C# COM server, I just reference the COM server dll in C#, however if I just want to call the COM server just like COM, how can I do it?

    Posted by Felix | July 23, 2013, 1:38 am
    • In another word, I got a exception “Unable to cast object of type ‘COMObject’ to type ‘COMObject’ ” to call C# COM server via C# codes, how can I resolve this issue?

      Posted by Felix | July 23, 2013, 1:40 am
  4. Great article, although you missed an important point. I was unable to instantiate my c# dll using a standard ‘CreatObject’ within vbscript after following this and many other similar blogs and articles.

    After much swearing and head scratching, I discovered that this was because the server I had deployed the dll to was a 64 bit machine. Although many articles mention the architecture when setting the project properties within Visual Studio (to use the AnyCPU platform), what doesn’t seem to be covered is the fact that there are different versions of RegAsm for 64bit and 32bit platforms! These can be found in “..Microsoft.NET\Framework\” and “Microsoft.NET\Framework64\” – you need to select the correct version depending on the client architecture of the host machine.

    Also, it’s worth noting that the waters can be further muddied if the dll is being called through IIS on a 64 bit machine. There is an option to Enable 32 applications for a specific application pool – if this is selected and you registered the dll using the 32 bit version of RegAsm, you’ll find the dll works when instantiated within an asp page but not if created directly.

    It would be really good if you could add this to your article when registering the dll – it could save somebody a few headaches!

    Posted by Mark Sci | August 30, 2013, 3:42 pm
  5. can you please demonstrate how to use c# client for the com server exe written in c++???

    Posted by Syed Natiquddin | October 9, 2013, 12:28 pm
  6. Very thanks! It worked like a charm.

    Posted by Nilson | November 11, 2014, 5:05 pm

Trackbacks/Pingbacks

  1. Pingback: .NET COM Component creation failed in 64bit process even Assembly platform target is AnyCPU - AsiaTech: Microsoft APGC Internet Developer Support Team - Site Home - MSDN Blogs - February 13, 2014

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: