0
votes

I'm working with a native C++ dll from C#. On the C++ side there's a class that only implements one interface and IUnknown but its QueryInterface returns E_NOINTERFACE for anything other than IUnknown.

On the C# side I'm calling a method in the dll that returns an instance of this class, but even if I declare the parameter as ISomeInterface .NET still calls QueryInterface which results in an exception. E.g.:

[ComImport, Guid(" ... ")]
public interface ISomeInterface { ... }

[DllImport("mylib.dll")]
public static extern void GetThing([MarshalAs(UnmanagedType.Interface)] out ISomeInterface thing);

calling GetThing throws InvalidCastException with message "This operation failed because the QueryInterface call on the COM component for the interface with IID '{ ... }' failed due to the following error: No such interface supported (Exception from HRESULT: 0x800040002 (E_NOINTERFACE))."

If I change the type to object

public static extern void GetThing([MarshalAs(UnmanagedType.Interface)] out object thing);

..then calling GetThing succeeds but trying to cast the resulting System.__ComObject to ISomeInterface throws the same exception.

I tried every different InterfaceType attribute on the interface definition but always get the same exception.

I also tried defining the interface as an abstract class:

[ComImport, Guid(" ... "), ClassInterface(ClassInterfaceType.None)]
public abstract class ISomeInterface { ... }

Calling GetThing then succeeds and I get an instance of ISomeInterface but as soon as I call any method it throws a BadImageFormatException with message "Bad IL format."

And I tried every different ClassInterfaceType attribute with the same results.

Is there any way this class can be used from C# without modifying the C++ code to fix its QueryInterface implementation?

2
Well that's unusual for a COM object to just recognise IUnknown and nothing else, any reason for that? Just so you know, the act of c# code casting an object to ISomeInterface will end up calling QueryInterface behind the scenes (when the object is a COM object of course) so that is to be expected. If you have access to the c++ code you should fix it.MickyD
On the C++ side ISomeInterface is the default interface so they're able to use it without QueryInterface - I'll change the C++ as well but would ideally get it working with this version for compatibilitygordy
"is the default interface" - Hmm that behavior may only work by default for scripting languages. Also, if it were the case why then do you get an E_NOINTERFACE error? Anyway, try updating the c++ code, due to the nature of COM it is unlikely you'll break compatibility unless you change GUIDs; interfaces etc which in this case is not a requirementMickyD
I've actually modified the C++ code, rebuilt and confirmed that I can get the class and use it in C# - but I'm still interested in any way to get it working without having to distribute a new/unofficial build of this dllgordy
You make a very curious statement in your opening paragraph. If calling QueryInterface on a COM interface pointer returns E_NOINTERFACE for a given interface, then by definition according to the COM rules, that object does not implement that interface. If your object "pretends" that it implements that interface, it necessarily does so by breaking the rules of COM, and any functionality you get out of it is essentially undefined behavior. Such C++ "COM object" is broken.Euro Micelli

2 Answers

0
votes

With this approach you mentioned:

public static extern void GetThing([MarshalAs(UnmanagedType.Interface)] out object thing);

...assuming you are using .NET 4+ try the following:

object thing;
GetThing (out thing); // prefix me with the class defining the `DllImport`
dynamic d = thing;
d.DoSomething();  //  CHANGE DoSomething() to a known method that ISomeInterface exposes

The behavior of dynamic with COM is that no typelibrary is required ahead of time. You will not see statement completion/Intellisense in Visual Studio as you type your code for the methods on the dynamic object but the code will compile. It will be evaluated at runtime. Hopefully that will bypass QueryInterface() and will make use of the default(ISomeInterface ) that you indicated your COM class is decorated with.

OP:

On the C++ side ISomeInterface is the default interface

OP:

...but I'm still interested in any way to get it working without having to distribute a new/unofficial build of this dll

If this works then no change to the COM DLL is required

0
votes

According to the rules of COM, an object by definition implements an interface only if calling QueryInterface on any interface pointer to that object with the UUID of the sought interface returns a corresponding new (AddRef'd) interface pointer (by which of course I mean returning S_OK and providing the interface pointer via the second argument).1

... there's a class that only implements one interface and IUnknown but its QueryInterface returns E_NOINTERFACE for anything other than IUnknown.

No, that's incorrect. That object does not implement such interface. If the object is "pretending" that it implements that interface, but does not follow the rules of COM, then that object has a major bug and is broken. Any functionality you happen to get from such situation is essentially undefined behavior.

Specifically, it is illegal for a method to return an interface pointer of type ISomething if calling QueryInterface on that pointer with ISomething returns E_NOINTERFACE (See corollary on the footnote). You have to fix the C++ implementation of that object.


1 There a couple of subtle points:

First, curiously the object doesn't have to decide whether it implements a given interface until someone asks for it (although most objects of course have a hardcoded list of interfaces).

Second, once the object has made its mind and announced the decision about a given interface (by either returning a new interface pointer, or E_NOINTERFACE), that particular object can never change its mind for the rest of its lifetime and must always provide the same answer. As a corollary, calling QueryInterface from a given interface pointer, asking for the same interface, must succeed. This is where your object is broken.

Third, I intentionally said "any" interface pointer: for example, if an object implements five interfaces beyond IUnknown, when asked about a 6th interface, it must provide the same answer when asked from any pointer to any of the five other interfaces, as well as from an IUnknown pointer. That is, all implemented interfaces must be reachable from all other interfaces.

Finally, notice that I've been talking about objects, not CoClasses. CoClasses, default interfaces, registry entries, type libraries, et al, those are all (somewhat optional) mechanisms that exist for the benefit of the standard "COM factory" infrastructure, and to assist the developers' tooling. Once you get an interface pointer to an object from that infrastructure, all there is is interface pointers and the underlying objects (which means that the phrase "is the default interface" is irrelevant, and does not absolve the object from following the rules of COM interfaces)

i recommend Don Box's "Essential COM", where you can find all this spelled out in great detail.