14

I have this interface in the dll (this code is shown in Visual Studio from metadata):

#region Assembly XCapture.dll, v2.0.50727
// d:\svn\dashboard\trunk\Source\MockDiagnosticsServer\lib\XCapture.dll
#endregion

using System;
using System.Runtime.InteropServices;

namespace XCapture
{
    [TypeLibType(4160)]
    [Guid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")]
    public interface IDiagnostics
    {
        [DispId(1)]
        void GetStatusInfo(int index, ref object data);
    }
}

So I created a COM server with such class:

[ComVisible(true)]
[Guid(SimpleDiagnosticsMock.CLSID)]
[ComDefaultInterface(typeof(IDiagnostics))]
[ClassInterface(ClassInterfaceType.None)]
public class SimpleDiagnosticsMock : ReferenceCountedObject, IDiagnostics
{
    public const string CLSID = "281C897B-A81F-4C61-8472-79B61B99A6BC";

    // These routines perform the additional COM registration needed by 
    // the service. ---- stripped from example

    void IDiagnostics.GetStatusInfo(int index, ref object data)
    {
        Log.Info("GetStatusInfo called with index={0}, data={1}", index, data);

        data = index.ToString();
    }
}

Server seems to work fine, and I am able to use the object from a VBScript. But then I try to use it from another C# client:

    [STAThread]
    static void Main(string[] args)
    {
        Guid mockClsId = new Guid("281C897B-A81F-4C61-8472-79B61B99A6BC");
        Type mockType = Type.GetTypeFromCLSID(mockClsId, true);
        IDiagnostics mock = (IDiagnostics)Activator.CreateInstance(mockType);

        //var diag = mock as IDiagnostics;

        object s = null;
        mock.GetStatusInfo(3, ref s);

        Console.WriteLine(s);
        Console.ReadKey();
    }

And it fails with

Unable to cast COM object of type 'System.__ComObject' to interface type 'XCapture.IDiagnostics'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).

What am I doing wrong?

I have also tried to use InvokeMember, and that kinda worked except that I wasn't able to get the ref-returned data parameter.

EDIT: added STAThread attribute to my Main procedure. This does not solve the issue, but you really should use STAThread with COM unless you're absolutely sure you don't need it. See Hans Passant's answer below.

catbert
  • 1,017
  • 1
  • 9
  • 19

2 Answers2

34

This exception can be a DLL Hell problem. But the simplest explanation is for what's missing from your snippet. Your Main() method is missing the [STAThread] attribute.

That's an important attribute that matters when you use COM objects in your code. Most of them are not thread-safe and they require a thread that's a hospitable home for code that cannot support threading. The attribute forces the state of the thread, the one you can set explicitly with Thread.SetApartmentState(). Which you can't do for the main thread of an app since Windows starts it, so the attribute is used to configure it.

If you omit it then you the main thread joins the MTA, the multi-threaded apartment. COM is then forced to create a new thread to give the component a safe home. Which requires all calls to be marshaled from your main thread to that helper thread. The E_NOINTERFACE error is raised when COM cannot find a way to do that, it requires a helper that knows how to serialize the method arguments. That's something that needs to be taken care of by the COM developer, he didn't do that. Sloppy but not unusual.

A requirement of an STA thread is that it also pumps a message loop. The kind you get in a Winforms or WPF app from Application.Run(). You don't have one in your code. You might get away with it since you don't actually make any calls from a worker thread. But COM components tend to rely on the message loop to be available for their own use. You'll notice this by it misbehaving, not raising an event or deadlocking.

So start fixing this by applying the attribute first:

[STAThread]
static void Main(string[] args)
{
    // etc..
}

Which will solve this exception. If you have the described event raising or deadlock problems then you'll need to change your application type. Winforms is usually easy to get going.

I cannot otherwise take a stab at the mocking failure. There are significant deployment details involved with COM, registry keys have to be written to allow COM to discover components. You have to get the guids right and the interfaces have to be an exact match. Regasm.exe is required to register a .NET component that's [ComVisible]. If you try to mock an existing COM component, and got it right, then you'll destroy the registration for the real component. Not so sure that's worth pursuing ;) And you'll have a significant problem adding a reference to the [ComVisible] assembly, the IDE refuses to allow a .NET program to use a .NET assembly through COM. Only late binding can fool the machine. Judging from the COM exception, you haven't gotten close to mocking yet. Best to use the COM component as-is, also a real test.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks for the answer, it helped me a lot in investigating the issue. However, nothing you mentioned was the reason. Well, that's COM for you :-) – catbert Jun 06 '13 at 10:43
  • In my question [Why can't I cast MsftDiscFormat2Data to IBurnVerification](http://stackoverflow.com/questions/18308542/why-cant-i-cast-msftdiscformat2data-to-iburnverification) I experience this very issue...But what helped me to have the cast work is **removing `[STAThread]`** from the `Program.cs`. Could you, please, give me a piece of advice, why that could be so? – horgh Aug 19 '13 at 07:42
5

So, the problem was that my DLL with IDiagnostics interface was generated from a TLB, and that TLB never got registered.

Since the DLL was imported from the TLB, RegAsm.exe refuses to register the library. So I used the regtlibv12.exe tool to register the TLB itself:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\regtlibv12.exe "$(ProjectDir)\lib\Diagnostics.tlb"

Then everything magically started to work.

Since regtlibv12 is not a supported tool, I still don't know how to do this properly.

catbert
  • 1,017
  • 1
  • 9
  • 19
  • 5
    Hmm, no, you found a workaround for the missing [STAThread] attribute. With the type library registered, normally done by the Regasm.exe /tlb option, the standard marshaller now works to marshal the calls from one thread to another. Which may be okay to keep you going but the core issue is still that the code is running on the wrong thread. Beware of other side-effects, like the great deal of overhead in the calls. Regtlibv12 is not a supported tool because it is always the wrong tool to use. The only reason it exists is because the .NET framework installer needed it. – Hans Passant Jun 06 '13 at 10:48
  • I would love to just agree with you, but in my specific case, [STAThread] attribute does not solve the problem. I still need to use regtlibv12, and still have no idea why do I need to use it. I'll update the code in question to reflect my usage of STAThread. – catbert Jun 07 '13 at 13:46
  • What is the context of this code, who starts it? Is there a unit test runner involved? Then that one determines the apartment state of the thread. Double-check with Thread.CurrentThread.GetApartmentState(). – Hans Passant Jun 07 '13 at 13:51
  • @HansPassant Well, I start it, either by pressing F5 in Visual Studio or running the executable from command prompt. GetApartmentState returns STA. – catbert Jun 07 '13 at 13:55
  • You are getting STA without using [STAThread]??? Or did you in fact use my answer? – Hans Passant Jun 07 '13 at 14:04
  • I did change my code to use STAThread as soon as I saw your answer. It just didn't help with the issue. Registering the tlb helped though. – catbert Jun 07 '13 at 14:15
  • Well, remove [STAThread] again and see if it still works. If it doesn't then this is not the correct answer. Why you had *two* problems isn't clear from the question. That you solved your second problem the wrong way is something you already know. Please don't mark the wrong answer, it isn't helpful to anybody. – Hans Passant Jun 07 '13 at 14:18
  • Whether I specify STAThread does not affect the result. Code works if I register TLB, nothing works if I don't. Registering TLB works, STAThread doesn't. Sorry if I caused confusion. – catbert Jun 07 '13 at 14:22
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/31409/discussion-between-catbert-and-hans-passant) – catbert Jun 07 '13 at 14:28
  • 1
    Yes the last comment about TLB being the problem resonates with me. I get an error like this very rarely, from some user - where the code is undoubtedly correct, as it works on hundreds of machines. Yet for some, where something went wrong with the registry, I get this. – PandaWood Jan 10 '16 at 23:43
  • 2
    what if you have no tlb? for example on a customer's machine? – Steinfeld Mar 14 '16 at 20:22