COM Single Threaded Apartment (STA) from a .NET perspective

Let us create a simple ATL COM library providing some COM object which can be used from .NET. The primary interop assembly for this COM library is generated by using tlbimp.exe from the Windows SDK.

Create a new ATL project named ComGeometry and use the default settings.

The wizard should generate two projects. The ComGeometry project contains the implementation and the ComGeometryPS project creating the proxy/stub library is maintained by VS. The proxy/stub library is necessary for marshalling calls from one apartment into another where each instance of the ComGeometry library is hosted.

Rule I: Any COM instance is bound to the lifetime of its apartment.
Rule II: A COM instance of a STA can only be directly accessed by the thread which created it. 

Add a new ATL simple object (right click the ComGeometry project than Add class) named Point2D. Make sure setting the threading model to apartment (STA). We do not want to use aggregation or implement ISupportErrorInfo and we can directly implement from IUnknown.


The wizard should generate a Point2D.h, Point2D.cpp and modify a bunch of files including the idl file. Alter the generated Point2D.h by adding two member variables for the x and y coordinates and declare the necessary getter and setter. You can use the VS wizard by right clicking on the Point2D class entry in the class view and choose add variable for x and y. After that right click on the IPoint2D interface entry and choose add property for x and y. Make sure to initialize the x and y coordinates with 0.

class ATL_NO_VTABLE CPoint2D :  public CComObjectRootEx,  public CComCoClass,  public IPoint2D
{
public:
    CPoint2D() : x(0), y(0)
    {  }
...
public:
    STDMETHOD(get_X)(DOUBLE* pVal);
    STDMETHOD(put_X)(DOUBLE newVal);
    STDMETHOD(get_Y)(DOUBLE* pVal);
    STDMETHOD(put_Y)(DOUBLE newVal);

private:
    double x;
    double y;

Implement these methods in the Point2D.cpp as follows.

STDMETHODIMP CPoint2D::get_X(DOUBLE* pVal)
{
    *pVal = x;
    return S_OK;
}
STDMETHODIMP CPoint2D::put_X(DOUBLE newVal)
{
    x = newVal;
    return S_OK;
}
STDMETHODIMP CPoint2D::get_Y(DOUBLE* pVal)
{
    *pVal = y;
    return S_OK;
}
STDMETHODIMP CPoint2D::put_Y(DOUBLE newVal)
{
    y = newVal;
    return S_OK;
}

Validate the idl file of the ComGeometry project.

interface IPoint2D : IUnknown
{
[propget] HRESULT X([out, retval] DOUBLE* pVal);
[propput] HRESULT X([in] DOUBLE newVal);
[propget] HRESULT Y([out, retval] DOUBLE* pVal);
[propput] HRESULT Y([in] DOUBLE newVal);
};

Make sure the code compiles and the COM library is correctly registered.
Now, we have to use tlbimp.exe by specifiying the ComGeometry.tlb.
For instance we can use a custom build step.
“…Microsoft SDKs\Windows\v7.1\Bin\Tlbimp.exe” $(IntermediateOutputPath)\$(TargetName).tlb /out: “$(TargetDir)DotNet$(TargetName).dll”
This should generate the DotNetComGeometry interop assembly.
Rebuild again and make sure the interop assembly is generated.
Add a new C# console application to the existing solution and add a reference to the generated DotNetGeometry assembly. Modify the code as follows.

using DotNetComGeometry;

namespace DotNetGeometryConsoleApplication
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            Point2D point = new Point2D();
            point.X = 1;
        }

While debugging the x coordinate should be set to 1.

Add the necessary using for the threading namespace. Now, we are creating a thread, set its apartment to STA and create the point instance. After the new thread created the point instance we are trying to modify this instance from the main thread. The call should be marshalled between both STA’s using the registered proxy/stub library. Marshalling between apartments can be a huge bottleneck (especially calling from a MTA into a STA see the next section). But this and missing thread-safetyness of the Point2D COM instance is not the focus of our interests. When the newly created thread goes down, the associated STA should also release all created COM instances. Regarding to Rule I this should lead to an exception. The Runtime Callable Wrapper (RCW) which interacts directly with the COM instance should be in an invalid state.

[STAThread]
static void Main(string[] args)
{
    // Create the point instance in a new thread
    Point2D point = null;
    var thread = new Thread(() => { point = new Point2D(); });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    // Wait for the thread
    thread.Join();

    // Accessing the point instance
    // when the thread and its STA maybe gone
    point.X = 1;
}

The RCW has lost its underlying COM instance.

If we create the point instance in the main thread and modify this instance in a separated thread the call should succeed.

[STAThread]
static void Main(string[] args)
{
    // Create the point instance in the main thread
    Point2D point = new Point2D();
    var thread = new Thread(() =>
    {
        // Accessing the point instance
        // should be valid
        point.X = 1;
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
    // Wait for the thread
    thread.Join();
}

A Multithreaded Apartment (MTA) can hosts many threads and has the same lifetime as the process. The MTA is created once and all threads having a MTA state will enter this MTA. If we change the apartment state for the newly created thread to MTA both listings should work. The created COM instances from the newly created thread should be valid till the process shutdowns the MTA.

[STAThread]
static void Main(string[] args)
{
    // Create the point instance in a new thread
    Point2D point = null;
    var thread = new Thread(() => { point = new Point2D(); });
    thread.SetApartmentState(ApartmentState.MTA);
    thread.Start();
    // Wait for the thread
    thread.Join();

    // Accessing the point instance
    // of the process-wide MTA should be valid
    point.X = 1;
}

The following listings using a thread pool and a task factory should also be valid cause their threads enter the MTA too.

[STAThread]
static void Main(string[] args)
{
    // Create the point instance in another thread
    // which entered the MTA
    Point2D point = null;
    var resetEvent = new ManualResetEvent(false);
    ThreadPool.QueueUserWorkItem(c =>
    {
        try
        {
            point = new Point2D();
        }
        finally
        {
            // Send the event
            resetEvent.Set();
        }
    });

    // Wait for the event
    resetEvent.WaitOne();

    // Accessing the point instance
    // should be valid
    point.X = 1;
}

 

[STAThread]
static void Main(string[] args)
{
    // Create the point instance in another thread
    // which entered the MTA
    Point2D point = null;
    var task = Task.Factory.StartNew(() =>
    {
        point = new Point2D();
    });
    // Define the continuation
    task.ContinueWith(t =>
    {
        // Accessing the point instance
        // should be valid
        point.X = 1;
    });
    // Wait for the task
    task.Wait();
}

 

[STAThread]
static void Main(string[] args)
{
    CreateAndUseAsync();
}

private async static void CreateAndUseAsync()
{
    // Create the point instance in another thread
    // which entered the MTA
    Point2D point = null;
    await Task.Factory.StartNew(() =>
    {
        point = new Point2D();
    });
    // Define the continuation
    // Accessing the point instance
    // should be valid
    point.X = 1;
}

Rule II defines that without the proxy/stub marshalling an instance of a STA cannot be used directly from another apartment. To achieve this we have to clean the solution and modify the configuration manager (context menu item of the solution entry).

The follwing listing should create an exception saying the instance cannot be cast into the necessary interface. We only have to change the apartment of the main thread to MTA.

[MTAThread]
static void Main(string[] args)
{
    // Create the point instance in the main thread
    // cross apartment call should throw an exception
    Point2D point = new Point2D();
    point.X = 1;
}