//
you're reading...
.NET/COM/ActiveX Interop, COM, Programming Issues/Tips, Type Information

Tip for Creating a COM Server Using C# – Apartment Marshaling and the Type Library.

1. Introduction.

1.1 I had earlier published Creating a COM Server Using C#. The aim of that article was to expound on the basic coding requirements for creating a COM server using the C# language.

1.2 This write-up is one of several follow-up articles that are intended to provide additional supplementary tips, techniques and knowledge useful for writing managed classes that are exported to COM.

1.3 The central theme behind the tip of this article : cross-apartment marshaling concerns and the relevance in this regard of the type library which has been generated from a COM-exported managed class library.

2. The Type Library.

2.1 As mentioned in the original article, once a COM-exported class library has been successfully compiled, it needs to be registered via REGASM.EXE.

2.2 And as part of the process of assembly registration, a type library can be generated (via the /tlb option). This type library is a very important file in that it contains the definition of COM types which are equivalent to the managed types defined within the class library.

2.3 Its purpose is to allow the source codes of unmanaged client applications to successfully compile with reference to these COM types.

2.4 An example of how the type library is referenced (via a #import statement) in a Visual C++ client application source code was demonstrated in the original article. The equivalent action to be done when building a Visual Basic 6.0 application project would be to reference the type library.

2.5 However, important as it is during client code development, the type library which is generated from a COM-exported managed class library is practically unused at runtime. Being used for development is as far as its relevance goes.

2.6 This assertion is the main focal point of this article. To help the reader understand this, I must briefly explain the concept of COM marshaling in general and that of the proxy in particular. These topics are touched in the sections that follow.

3. Brief Primer on COM Marshaling.

3.1 In simple words, marshaling means the act of transporting something from one place to another.

3.2 In the COM sub-system, this something that is transported could be the following :

  • COM interface pointer.
  • Individual parameters to a method call.
  • Return value from a method call.

3.3 Note that it is actually an object’s interfaces that are marshaled/unmarshaled and not the object itself. Furthermore, an interface is marshaled/unmarshaled from/to apartments, not threads.

3.4 In general, there are 2 types of marshaling :

  • Standard marshaling.
  • Custom marshaling.

Both are conducted under the aegis of the generic marshaling architecture code contained in OLE32.DLL. It is also possible to select type of marshaling to be used at runtime.

3.5 Marshaling requires a marshaler which is a COM object which must implement the IMarshal interface. Its job scope in general is as follows :

  • Indicate the CLSID of its conjugate Proxy object (see section 4).
  • Create a marshaling data packet which serves as initialization data for its associated proxy.
  • Communicate with its proxy at runtime to effect cross-apartment calls.

When a marshaler is required, COM inquires the object (whose interface is to be marshaled) for an IMarshal interface. The object, if it implements its own marshaler, will return an IMarshal interface pointer.

3.6 Custom marshaling is an interesting topic of its own (see the link in point 4.9 to a CodeProject article that discusses this in greater detail), However, we shall not be go any deeper into this since it has no relation with type libraries in general.

3.7 By standard marshaling, we mean the built-in marshaling system provided by the COM sub-system. Standard marshaling is widely used. It comes in 2 flavours :

  • Type Library Marshaling.
  • Proxy/Stub Marshaling.

3.8 Whichever type of marshaling system is chosen, certain essential information involved with the act of marshaling are required and must be available at runtime : e.g. information regarding interfaces, method parameter types and return value types.

The way these information is obtained is intrinsic upon the type of standard marshaling used. We will examine each of these marshaling systems in more detail below.

3.9 On Type Library Marshaling.

Type library marshaling is marshaling which is driven by information contained in a type library. The type library contains rich information on COM types which can be used for marshaling purposes. The standard proxy and stub code as contained in OLEAUT32.DLL (the standard proxy/stub dll for Ole Automation) is used to drive type library marshaling.

A natural requirement is that the type library be registered. It is by being registered that the standard marshaler be informed that type library marshaling is to take place and where to locate the type library.

Of course, the definition of the interface to be marshaled must be contained in the type library. In order to facilitate this, the definition of the marshaled interface must either be defined inside the “library” statement in the source IDL file, or is at least referenced inside the “library” statement. Otherwise, marshaling will fail. One of the possible failure code is TYPE_E_ELEMENTNOTFOUND (0x8002802B).

Another requirement is that the methods and properties of the marshaled interface must strictly use types that are automation-compatible (i.e. the types that can be found in a VARIANT structure).

And finally, the interface definition must be attributed with either “oleautomation” or “dual” or that it is by definition a “dispinterface”. This will ensure that the Type Library Marshaler (contained inside OLEAUT32.DLL) recognizes the interface as being OLE-automation compatible. Failure to comply will result in failure in marshaling. One of the possible failure code is E_FAIL (0x80004005).

For a managed interface which is attributed by the InterfaceTypeAttribute with ComInterfaceType.InterfaceIsDual or ComInterfaceType.InterfaceIsIDispatch as constructive argument, the counterpart COM interface will be OLE-automation compatible.

3.10 On Proxy/Stub DLL Marshaling.

This type of marshaling requires a proxy/stub DLL which contains specialized code that is optimized for marshaling interfaces which are non-oleautomation-compatible (i.e., the interfaces are based on IUnknown).

The proxy/stub dll is built from source codes which are generated by MIDL.EXE. There is currently no Visual Studio support for either generating proxy/stub dlls for .NET based interfaces or for generating source codes that can be used to build one.

As such, we shall not be studying proxy/stub marshaling any further in this article.

4. Brief Primer on the Marshaling Proxy.

4.1 A COM interface pointer which has been marshaled to another apartment is also known as a proxy. A proxy is a representative of the same interface of the original object in a foreign apartment. The proxy is actually a COM object in its own right.

4.2 When a method of an interface is called inside a thread of a foreign apartment, it is the proxy’s responsibility to transfer control back to the object’s own apartment and ensure that the method invocation is executed inside the original apartment.

4.3 Besides the COM interface, the proxy is also responsible for marshaling method parameters and return values.

4.4 Since the primary objective of a COM interface proxy is to ensure that a method call gets executed in the original apartment of the object, when an interface method of a proxy object is executed, the proxy must somehow pass control to the original object (in its original apartment), get it to execute the method and then return control back to the proxy.

4.5 Before the method is executed by the original object, all parameters to the method must be marshaled from the calling thread to the thread of the apartment of the original object. This may involve simple and/or complex data copying.

4.6 How all these are performed depends on the implementation of the proxy itself. However, at minimum, a proxy must also implement the IMarshal interface as well as the methods of the interface that it is supposed to represent.

4.7 The proxy is closely connected with the marshaler used for the original object. The relationship between the marshaler and the proxy will be explained in section 5 below.

4.8 Just as there are standard and customized marshalers, there are also standard and customized proxies. Throughout this article, we shall concentrate only on standard marshalers and proxies which is widely used.

4.9 For further reading on COM marshaling, please refer to the following CodeProject articles :

5. The Marshaling Process.

5.1 As first mentioned in point 3.5 above, when a COM interface is to be marshaled from one apartment to another, COM will first inquire the object behind the interface whether it supports the IMarshal interface.

5.2 If it does, it means that the object will supply its own customized marshaler and proxy.

5.3 If it does not, it means that it wants to use the standard marshaler and proxy as provided by the system.

5.4 Whichever marshaler is indicated, COM will inquire this marshaler for the CLSID of the proxy object to be created in the target apartment. This is done via the IMarshal::GetUnmarshalClass() method.

5.5 As part of proxy initialization, COM will arrange for the marshaler to pass something known as a marshal data packet to the newly created proxy. This is done to help the object establish its communication protocol with its proxy.

5.6 Once the marshaler and its proxy have established connection, the communication protocol is totally within the domain of the marshaler and its proxy. COM plays no part in the communication process.

5.7 When a method of the marshaled interface is called, the proxy must possess knowledge on how to copy the parameters from the stack of the thread of the foreign apartment to the stack of the thread of the original apartment. For the type-library-driven standard marshaler/proxy system, this knowledge comes from none other than the type library.

5.8 While the original object is executing the method, the proxy’s thread must halt until the method is completed. Control is thereafter returned to the proxy. When this happens, the return value from the method call which has been performed in the original apartment must be marshaled back to the calling apartment and then used as the return value.

5.9 COM objects which are implemented in managed code are subject to the same overall protocol. After all, the fact that they are CCWs is transparent to the COM system.

5.10 When a CCW is subject to cross-apartment marshaling, what exactly will be the answer when COM queries it for the IMarshal interface ? We will begin our investigation into this starting from the next section.

6. COM Interop Marshaling.

6.1 With the advent of the .NET framework, a new situation has arisen : COM interop marshaling. By this we mean the marshaling of interfaces, method parameters and return values between managed and unmanaged code.

6.2 The objective of COM interop marshaling is exactly the same as that of COM marshaling : to ensure that an interface method is executed by an object in the object’s context.

6.3 The object’s “context” means “apartment” when applied to COM objects.

6.4 In the case of a COM interface being implemented by a managed object and used in unmanaged code, “context” would mean the managed environment.

7. What Happens when a Method of a Managed COM Object is Invoked.

7.1 When a CCW wrapped managed object is instantiated in unmanaged code, it will behave just like a COM object in the sense that it will belong to an apartment.

7.2 A CCW wrapped managed object is always marked as supporting both STA and MTA apartments and so it will live in the same apartment as the owning thread (which is usually an STA or an MTA).

7.3 When a method of the object is called in the owning apartment, no COM marshaling will take place. However, within the code of the CCW, COM interop marshaling will occur. After all, the executable code for the method lies in the assembly which contains the actual object.

7.4 When this COM interop marshaling is performed, parameters to the method (if any) must be marshaled across from unmanaged code to managed code.

7.5 Now take note that information on the signature of the method (which includes information on the types used for the various parameters as well as the return value of the method call) will already be known by the containing assembly (through the metadata of the assembly).

7.6 In fact, information on the entire interface that is being used by the client application is available for the containing assembly.

7.7 When COM-interop marshaling takes place, the CCW relies totally on this metadata to effect a method call. The interop marshaler component of the CLR is likely involved in the process. Reflection is also likely used.

7.8 This being the case, the COM type library that was originally generated by REGASM.EXE and which was used by the unmanaged client for development is not required. In fact, from the point of view of the managed object, its metadata is of greater quality and accuracy than the translated information of the type library.

7.9 This is the reason why the Type Library is not needed as far as COM interop marshaling is concerned.

7.10 But what about when cross-apartment marshaling is to be performed ? Since we are simply trying to enusure synchronized access to the CCW COM object from across apartments, wouldn’t the type library be used ? Read on.

8. What Happens when a Method of a Managed COM Object is Called from a Foreign Apartment.

8.1 Now, when an interface pointer from a CCW wrapped managed object is to be used in an apartment aside from the one in which it was created, it must be COM marshaled to the target apartment just like any other COM interface pointer.

8.2 When this happens, COM will perform a QueryInterface() call to the interface pointer for the IMarshal interface as mentioned in point 5.1.

8.3 The CCW will indeed return an IMarshal interface pointer in response which indicates that it will supply its own custom marshaler.

8.4 COM will then use the returned IMarshal interface and call its IMarshal::GetUnmarshalClass() method. This method will return the CLSID of the proxy that is to be created at the target apartment. This custom proxy is the “Com Call Wrapper Unmarshal Class”. It has the CLSID : {3F281000-E95A-11d2-886B-00C04F869F04}. This object is contained in MSCOREE.DLL.

8.5 The time the proxy is created and fully initialized is the time the interface is successfully marshaled to the target apartment. The interface is not fully represented by the proxy (recall from point 4.6 that a proxy must implement all the methods of the interface that it represents).

8.6 Now when a method call is made on the proxy, does the proxy arrange things such that the method gets executed by the original CCW on the original apartment ? Seems the appropriate thing to do but no, it does not. Instead, the proxy communicates directly with the managed object that it ultimately represents.

8.7 It does so because the proxy and the original CCW both contain information on the managed object and its containing assembly. A direct link to the managed object via COM interop is possible and it is the preferred way to eventually communicate with it. This is the value-added work done by the “Com Call Wrapper Unmarshal Class” proxy.

8.8 Hence the interface method is executed by the same COM interop mechanism as described in section 7. This is why the type library is still not used even though an interface from the CCW has been marshaled to another apartment.

8.9 The standard type-library marshaling technique from the foreign apartment to the original one is grosssly inefficient. It requires 2 marshaling steps : first from the foreign apartment to the original one via apartment marshaling and then from unmanaged to managed code via COM interop marshaling.

8.10 Note also that because managed objects have no knowledge of apartments, a direct call from the foreign apartment to the managed object can be done in the same thread. Thereby optimizing performance even further.

9. Sample Codes.

9.1 I shall finish this article with some sample codes which will hopefully cement the reader’s understanding of the concepts explained in this article.

9.2 Listed below is a complete source codes for a simple COM-exported C# class which implements a simple interface which is exported to COM as a dual interface :

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

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()
        {
            MessageBox.Show(m_string_property);
        }

        public string string_property
        {
            get
            {
                return m_string_property;
            }

            set
            {
                m_string_property = value;
            }
        }

        private string m_string_property;
    }
}

The following is a summary of the code above :

  • A managed interface IInterface01 is defined. This interface is COM-visible and is attributed such that it will be a dual interface when exported to COM.
  • CSCOMClass01 is a COM-visible managed class derived from IInterface01. It will be exported to COM fronted by the IInterface01 interface without any class interface.
  • Its Method01() method implementation displays the value of its string_property property.

9.3 The following is a listing of a VC++ source code which demonstrates cross-apartment interface marshaling :

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

#include "stdafx.h"
#include <atlbase.h>  // Include this header in order to use AtlWaitWithMessageLoop().
#import "CSCOMServer.tlb" raw_interfaces_only no_implementation
using namespace CSCOMServer;

void DetermineMarshalingInfo(IInterface01Ptr& spIInterface01);

// Generic CreateInstance() templated function to help in generic object creation.
template <class SmartPtrClass>
bool CreateInstance(LPCTSTR lpszProgID, SmartPtrClass& spSmartPtrReceiver)
{
  HRESULT hrRetTemp = S_OK;
  _bstr_t bstProgID(lpszProgID);
  CLSID clsid;
  bool bRet = false;

  hrRetTemp = CLSIDFromProgID
  (
    (LPCOLESTR)bstProgID, //Pointer to the ProgID
    (LPCLSID)&clsid //Pointer to the CLSID
  );

  if (hrRetTemp == S_OK)
  {
    if (SUCCEEDED(spSmartPtrReceiver.CreateInstance(clsid)))
    {
	  bRet = true;
    }
    else
    {
	  bRet = false;
    }
  }

  return bRet;
}

template <class SmartPtrClass>
DWORD RegisterObjectToGIT(SmartPtrClass& spSmartPtr)
{
  IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;
  DWORD dwGITCookie = 0;

  CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );

  if (pIGlobalInterfaceTable)
  {
    IUnknownPtr spIUnknown = NULL;

    spSmartPtr -> QueryInterface(&spIUnknown);

    pIGlobalInterfaceTable -> RegisterInterfaceInGlobal
    (
      (IUnknown*)spIUnknown,
      IID_IUnknown,
      &dwGITCookie
    );

    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }

  return dwGITCookie;
}

template <class SmartPtrClass>
void GetObjectFromGIT(DWORD dwGITCookie, SmartPtrClass& spSmartPtrReceiver)
{
  IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;

  CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );

  if (pIGlobalInterfaceTable)
  {
    IUnknownPtr spIUnknown = NULL;

    pIGlobalInterfaceTable -> GetInterfaceFromGlobal
    (
      dwGITCookie,
      IID_IUnknown,
      (void **)&spIUnknown
    );

    spIUnknown -> QueryInterface(&spSmartPtrReceiver);

    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }
}

void RevokeInterfaceObjectFromGIT(DWORD dwGITCookie)
{
  IGlobalInterfaceTable* pIGlobalInterfaceTable = NULL;

  CoCreateInstance
  (
    CLSID_StdGlobalInterfaceTable,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IGlobalInterfaceTable,
    (void **)&pIGlobalInterfaceTable
  );

  if (pIGlobalInterfaceTable)
  {
    pIGlobalInterfaceTable -> RevokeInterfaceFromGlobal(dwGITCookie);
    pIGlobalInterfaceTable -> Release();
    pIGlobalInterfaceTable = NULL;
  }
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
  ::CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  DWORD dwGITCookie = (DWORD)lpParameter;
  IInterface01Ptr spIInterface01 = NULL;

  GetObjectFromGIT(dwGITCookie, spIInterface01);

  if (spIInterface01)
  {
	  BSTR bstr_string_property = ::SysAllocString(L"Hello World");

	  spIInterface01 -> put_string_property(bstr_string_property);

	  spIInterface01 -> Method01();

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

  ::CoUninitialize();

  return 0;
}

void DetermineMarshalingInfo(IInterface01Ptr& spIInterface01)
{
	IMarshalPtr spIMarshal = NULL;

	spIInterface01 -> QueryInterface(&spIMarshal);

	CLSID clsid;

	if (spIMarshal)
	{
		spIMarshal -> GetUnmarshalClass
		(
			__uuidof(IInterface01Ptr),
			NULL,
			MSHCTX_INPROC,
			NULL,
			MSHLFLAGS_NORMAL,
			&clsid
		);
	}
}

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

	if (1)
	{
		IInterface01Ptr spIInterface01 = NULL;

		CreateInstance(L"CSCOMServer.CSCOMClass01", spIInterface01);

		if (spIInterface01)
		{
			BSTR bstr_string_property = ::SysAllocString(L"Hello World");

			spIInterface01 -> put_string_property(bstr_string_property);

			spIInterface01 -> Method01();

			::SysFreeString(bstr_string_property);
			bstr_string_property = NULL;

			DWORD dwGITCookie = RegisterObjectToGIT(spIInterface01);

			DWORD  dwThreadId = 0;

			HANDLE hThread = CreateThread
			(
				NULL,
				0,
				ThreadProc,
				(LPVOID)dwGITCookie,
				0,
				&dwThreadId
			);

			// If an unmanaged COM object is in use, then
			// AtlWaitWithMessageLoop() must be used.
			//
			// If a managed COM object is in use, then
			// WaitForSingleObject() may be used.
			//WaitForSingleObject(hThread, INFINITE);
			AtlWaitWithMessageLoop(hThread);

			CloseHandle(hThread);
			hThread = NULL;

			RevokeInterfaceObjectFromGIT(dwGITCookie);

			DetermineMarshalingInfo(spIInterface01);
		}
	}

	::CoUninitialize();

	return 0;
}

The following is a summary of the code above :

  • CreateInstance<>() is a helper function that performs the creation of a COM object via a prog ID string. The returned interface pointer is wrapped inside a smart pointer class.
  • RegisterObjectToGIT<>(), GetObjectFromGIT<>() and RevokeInterfaceObjectFromGIT() are helper functions which facilitates the use of the Global Interface Table for sharing interface pointers across apartments.
  • DetermineMarshalingInfo() is a helper function which helps to determine the CLSID of the proxy of a custom marshaler.
  • The real action begins in _tmain() where an instance of the managed CSCOMClass01 class is created through its prog ID “CSCOMServer.CSCOMClass01”.
  • The instance is represented by an IInterface01 interface pointer.
  • Its string_property property is set and its Method01() method is called inside the original apartment.
  • The calls go through fine without COM marshaling. However, under the covers, COM interop takes place through the help of the CCW.
  • The IInterface01 interface pointer is then registered to the Global Interface Table (GIT) where it can be marshaled to any apartment.
  • A new thread fronted by ThreadProc() is started and the current thread is made to wait (via AtlWaitWithMessageLoop()) until the new thread terminates.
  • The ThreadProc() thread is initialized as an STA thread.
  • The GIT is used to retrieve the marshaled IInterface01 interface from the managed object in the main thread.
  • The retrieved IInterface01 interface pointer is used to set the object’s string_property property and to call its Method01() method.
  • The calls will go through successfully with no help from the type library (CSCOMServer.tlb) of the CSCOMServer.dll class library.
  • The calls would be successful even if the type library was deleted or renamed.
  • Furthermore, through debugging, it can be verified that the properties and methods of the managed object can be invoked from the same thread as that of ThreadProc().
  • The ThreadProc() thread will thus terminate and AtlWaitWithMessageLoop() will return.
  • The original IInterface01 interface pointer is then revoked from the GIT.
  • Before _tmain() finally ends, a call is made to DetermineMarshalingInfo() which will query the IInterface01 interface pointer (of the main apartment) for its IMarshal interface.
  • We know that the CSCOMClass01 object is a managed object wrapped by a CCW. The CCW exposes its own marshaler and proxy. Hence a QueryInterface() call to the IInterface01 interface pointer for an IMarshal interface would be successful.
  • The IMarshal::GetUnmarshalClass() method is called to obtain the CLSID of the proxy object associated with the CCW. The return value will be {3F281000-E95A-11d2-886B-00C04F869F04} which is the CLSID for the “Com Call Wrapper Unmarshal Class”.

9.4 A Word about AtlWaitWithMessageLoop().

Note that as an alternative to AtlWaitWithMessageLoop(), WaitForSingleObject() can also be used. Under normal circumstances where standard marshaling is used and where the COM object is STA-based and is created inside an STA and is marshaled across to another apartment, the owning apartment must contain a message loop in order that the object be accessible from the other apartment.

However, in the special case of a CCW-wrapped managed object, this message loop is not necessary. This is because although an interface from the object is marshaled to the ThreadProc() thread (which belongs to another STA apartment), the proxy does not communicate with the original CCW to effect any property setting or method call. It communicates directly with the managed object via COM interop as mentioned in section 8 and in the summary of point 9.3.

10. In Conclusion.

10.1 I started out hoping to write a relatively light essay expounding on a small tip but ended up submitting something much heavier.

10.2 Nevertheless I hope that the reader has benefitted from the organized effort of this treatise and that the research on the various types of marshaling and proxies have provided additional value.

10.3 The conclusion, nevertheless, is simple : the type library is not used as far as cross-apartment access to the interfaces of CCW-wrapped managed objects are concerned.

10.4 In summary, managed COM servers do not need the type library at runtime for cross-apartment calls due to 3 facts :

  • A managed object’s properties and methods are reached by COM interop mechanisms that rely on metadata contained in assemblies.
  • Unmanaged cross-apartment marshaling between a CCW-based proxy its original CCW-based object is redundant since the proxy is intrinsically able to directly communicate with the managed object.
  • Managed objects have no concept of apartments.

 

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

Trackbacks/Pingbacks

  1. Pingback: Creating a COM Server Using C#. « limbioliong - December 6, 2011

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: