Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Loading COM object in C# throws exception “Unable to cast COM object of type 'System.__ComObject' to interface type ...”, but C++ or VB not

I need make COM server in unmanaged C++, and COM client in C#. I found tutorial COM Hello World in C++ ( http://antonio.cz/static/com/5.html ). Page is in Czech language. COM server shows MessageBox with text “Hello world” after call function Print() from IHello interface. Source code is here: http://antonio.cz/static/com/Hello.zip . The archive contains source code of the COM server and COM client in C++ and it works.

But my C# COM client doesn't work. It is a C# console application with reference to “Interop.Hello.dll”. I make the interop dll with command:

tlbimp Hello.tlb /out:Interop.Hello.dll

C# code:

static void Main(string[] args)
{
    Interop.Hello.IHello Hello = new Interop.Hello.CHello();
    Hello.Print();
}

But C# client throws exception:

Unable to cast COM object of type 'System.__ComObject' to interface type
'Interop.Hello.CHello'. This operation failed because the QueryInterface call on the
COM component for the interface with IID '{B58DF060-EAD9-11D7-BB81-000475BB5B75}' 
failed due to the following error: No such interface supported (Exception from 
HRESULT: 0x80004002 (E_NOINTERFACE)).

I tried load COM server from Visual Basic too. And it works. I made console application in VB with reference to “Interop.Hello.dll”.

VB code:

Module Module1
    Sub Main()

        Dim ic As Interop.Hello.CHello

        ic = CreateObject("MyCorporation.Hello")
        ic.Print()

    End Sub
End Module

I debugged COM server when was loading from C# client. The method QueryInterface() return S_OK when in variable “riid” is IHello interface guid.

Any ideas why C# code doesn't work?

like image 540
dli Avatar asked Nov 30 '22 05:11

dli


2 Answers

No such interface supported

The error message is ambiguous. Everybody will assume it is their interface that isn't supported, IHello in your case. But that's not the case and the error message doesn't make that clear enough. It is the IMarshal interface that is not supported.

COM takes care of a programming detail that .NET does not, it doesn't ignore threading. Threads are notoriously difficult to get right, there is lots of code that is not thread-safe. .NET allows you to use such code in a worker thread and won't object to you getting it wrong, usually producing a very hard to diagnose bug. The COM designers originally thought that threading was too hard to get right and should be taken care of by the smart people. And built in the infrastructure to make using code that isn't thread-safe in a worker thread safe anyway. Which works pretty well, it takes care of 95% of the typical threading problems. The last 5% however tends to give you a rather major headache. Like this one.

A COM component, like yours, can publish whether it is safe to be used from a thread in the registry. The registry value name is "ThreadingModel". A very common value, also the default when it is missing, is "Apartment". Explaining apartments is a bit beyond the scope of this answer, it really means "I'm not thread-safe". The COM infrastructure ensures that any calls on the object are made from the same thread that created the object, thus ensuring thread-safety.

That however requires some magic. Marshaling a call from one thread to a specific other thread is a very non-trivial thing to do. .NET makes it look simple with methods like Dispatcher.BeginInvoke and Control.BeginInvoke but that hides a rather large iceberg of code that's 99% under water. And COM has a hard time doing this, it is missing a .NET feature that makes this easier to implement, it doesn't directly support Reflection.

What is required, for one, is building a stack frame on the target thread so that the call can be made. Which requires knowing what the arguments for the method looks like. COM needs help with this, it doesn't know what they look like since it cannot rely on Reflection. What is required is two pieces of code, called the proxy and the stub. The proxy does know what the arguments look like and serializes the arguments of the method into a RPC packet. That code is automatically called by COM, using a dummy interface that looks exactly like the original interface but with every method making a proxy call. On the target thread, the stub code receives the RPC packet, builds the stack frame and makes the call.

This might all sound familiar in .NET terms, this is exactly the way that .NET Remoting and WCF works. Except that .NET can automatically create the proxy and stub thanks to Reflection. In COM, they need to be created by you. Two basic ways that's done, the general way is to describe the COM interfaces in a language called IDL and compile that with the midl.exe tool. Which can auto-generate the proxy and stub code from the interface descriptions in the IDL. Or there's a simple way, available when your COM server restricts itself to the Automation subset and can generate a type library. In which case you can use a proxy/stub implementation that's built into Windows, it uses the type library to figure out what the arguments look like. Which is really a lot like Reflection. With the extra step of having to register this in the registry, HKCR\Interfaces key, so COM can find the code it needs.

So what the exception message really means is that COM cannot find a way to marshal the call. It looked in the registry and could not find the registry key for the proxy/stub. It then asked your COM object "do you know how to marshal yourself?" by querying for IMarshal. The answer was No! and that was the end of it, leaving you with an exception message that is rather hard to interpret. Error reporting is COM's Achilles heel.


Next, I'll need to focus on why COM decided that it should marshal the calls to your COM server, the thing that you did not expect to happen. One basic requirement for threads that makes calls to COM objects is that it needs to tell COM what kind of support it gives for marshaling calls. Which is the second thing that's hard to do beyond building the stack frame, the call needs to be made on a very specific thread, the one that created the COM object. The code that implements the thread needs to make this possible and that is not a trivial thing to do. It requires solving the general producer/consumer problem, a generic problem in software engineering. Where the "producer" is the thread that made the call and the "consumer" is the thread that created the object.

So what the thread has to tell COM is "I implemented a solution to the producer/consumer problem, go ahead and produce at will". The universal solution to the problem is well known to most Windows programmers, it is a "message loop" that a GUI thread implements.

You tell COM about it very early, every thread that makes COM calls must call CoInitializeEx(). You can specify one of two options, you can specify COINIT_APARTMENTTHREADED (aka STA) to promise that you give a safe home to COM objects that are not thread-safe. There's the word "apartment" again. Or you can specify COINIT_MULTITHREADED (aka MTA), which basically says that you do nothing to help COM and leave it up to the COM infrastructure to sort it out.

A .NET program doesn't call CoInitializeEx() directly, the CLR makes the call for you. It also needs to know whether your thread is STA or MTA. You do so with an attribute on your Main() method for the main thread of your program, specifying either [STAThread] or [MTAThread]. MTA is the default, also the default and only option for threadpool threads. Or when you create your own Thread you specify it by calling Thread.SetApartmentState().

The combination of MTA and a COM object that is not thread-safe, or in other words the "I do nothing to help COM" scenario is part of the problem here. You force COM to give the object a safe home. The COM infrastructure will create a new thread, automatically, an STA thread. It has to, no other way to ensure the calls on the object will be thread-safe since you opted out of helping. So any call you make on the object will be marshaled. That's pretty inefficient, creating your own STA thread avoids the marshaling cost. But most significantly, COM will require the proxy and stub to make the call. You didn't implement them so that's a kaboom.

This worked in your C++ client code because it probably called CoInitialize(). Which selects STA. And it worked in your VB.NET code because the vb.net runtime support automatically selects STA, typical for the language, it does lots of things automatically to help programmers fall in the pit of success.

But that's not the C# way, it does very few things automatically. You got the kaboom because your Main() method doesn't have the [STAThread] attribute so it defaults to MTA.

Do however note that this isn't actually the technically correct solution. When you promise STA you also have to fulfill that promise. Which says that you solve the producer/consumer problem. Which requires that you pump a message loop, Application.Run() in .NET. You didn't.

Breaking that promise can have unpleasant consequences. COM will rely on your promise and will try to marshal a call when it needs to, expecting it to work. It will not work, the call won't get dispatched on your thread since you are not calling GetMessage(). You are not consuming. You can readily see this with a debugger, the thread will deadlock, the call simply never completes. Apartment threaded COM servers often also readily assume that your STA thread pumps a message loop and will use it to implement their own inter-thread marshaling, typically by calling PostMessage() from a worker thread. The WebBrowser control is a good example. The side-effect of that PostMessage() message not going anywhere is, typically, that the component will not raise an event or otherwise not perform a duty. In the case of WebBrowser, you'll never get the DocumentCompleted event for example.

Sounds like your COM server doesn't make those assumptions and that you otherwise don't make calls on a worker thread. Or you would have noticed it malfunctioning with your C++ or VB.NET client code. That's a dangerous assumption that can byte any time, but you might well get away with it.

like image 138
Hans Passant Avatar answered Dec 05 '22 15:12

Hans Passant


Right C# code:

 [STAThread]
 static void Main(string[] args)
 {
     Interop.Hello.IHello Hello = new Interop.Hello.CHello();
     Hello.Print();
 }
like image 24
dli Avatar answered Dec 05 '22 16:12

dli