I have a problem in a utility library, which does some COM interop. It keeps references to COM objects which are used between calls.
If all methods are called from threads using the same COM threading model, the class works fine.
But if the calls that create COM objects use a different threading model than used for subsequent calls, QueryInterface fails with E_NOINTERFACE.
We only found this when we added async branches to our unit tests; prior to this it was running fine in all-MTA apps all-STA unit tests...
I think I understand the reason for the failure (via COM docs, Chris Brumme's blog) - the COM objects being used support "both" threading models which causes C# to create a fence between the STA and MTA-created instances.
However from the library's point of view, the only fixes I can think of are a bit rubbish:
CurrentThread.ApartmentState) Are there any cleaner/easier options? Here's a MCVE:
class Program
{
[ComImport, Guid("62BE5D10-60EB-11d0-BD3B-00A0C911CE86")] class SystemDeviceEnum { };
[ComVisible(true), ComImport, Guid("29840822-5B84-11D0-BD3B-00A0C911CE86"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICreateDevEnum { [PreserveSig] int CreateClassEnumerator([In] ref Guid pType, [Out] out IEnumMoniker ppEnumMoniker, [In] int dwFlags); }
static ICreateDevEnum createDeviceEnum;
static Guid VideoInputDeviceCategory = new Guid("860BB310-5D01-11d0-BD3B-00A0C911CE86");
static void Prepare()
{
var coSystemDeviceEnum = new SystemDeviceEnum();
createDeviceEnum = (ICreateDevEnum)coSystemDeviceEnum;
}
static int GetDeviceCount()
{
IEnumMoniker enumMoniker;
createDeviceEnum.CreateClassEnumerator(ref VideoInputDeviceCategory, out enumMoniker, 0);
if (enumMoniker == null) return 0;
int count = 0;
IMoniker[] moniker = new IMoniker[1];
while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0) count++;
return count;
}
[STAThread]
static void Main(string[] args)
{
RunTestAsync().Wait();
}
private static async Task RunTestAsync()
{
Prepare();
await Task.Delay(1);
var count = GetDeviceCount();
Console.WriteLine(string.Format("{0} video capture device(s) found", count));
}
}
COM threading is notoriously poorly understood. Actually much, much easier to get going than threading .NET classes. Just about everybody knows that, say, the List<> or Random class is not thread-safe. Not that many know how to use them in a thread-safe way. The COM designers had much loftier goals and assumed that programmers in general don't know how to write thread-safe code and that the Smart People should take care of it.
It does require taking care of a few details. First and foremost, you must tell COM what kind of support you are willing to provide to coclasses that are not thread-safe but are used from a worker thread anyway. And there you committed a horrible, horrible crime. When you use [STAThread] then you make a promise. Two things you must do: you must never block the thread and you must pump a message loop (aka Application.Run). Note how you broke both requirements. Never ever lie about it, very bad things happen when you do. But you didn't get that far yet.
The kind of threading support you can expect from the coclass you are using is easy to discover. Start up Regedit.exe and navigate to HKLM\Software\Wow6432Node\Classes\CLSID. Locate the {guid} you use and look at the ThreadingModel value you see in the InProcServer32 key. It is "Both" for the one you are using. Means that it was written to work both from an STA thread and a thread that doesn't support thread-safety at all and runs in the MTA. Like your main thread and your Task. And as you discovered, it works fine from either. Do beware that this isn't very usual, the vast majority of COM servers only support the "Apartment" threading model. Microsoft usually goes the extra thousand miles to support both.
So you created the enumerator object on an STA thread and use it on a thread in the MTA. Now the COM runtime must do something quite non-trivial, it must make sure that any callbacks (aka events) that might be invoked from the method you call run on the same STA thread so that any code in the callback is thread-safe as well. In other words, it must marshal the call from the worker thread back to your main thread. The equivalent of Control.Invoke or Dispatcher.Invoke in a .NET app. Done completely automatically in COM.
That requires doing something that's very easy in .NET but very hard in unmanaged code. The arguments of the method must be copied from one stack frame to another so the call can be made on the other thread. Easy to do in .NET thanks to Reflection. Not nearly as easy to do for unmanaged code, it requires an oracle that knows what the method parameter types are, a substitute for the missing metadata.
That oracle is found in the registry as well. Use Regedit and navigate to the HKLM\Software\Wow6432Node\Classes\Interface key. Locate the interface guid there, {29840822-5B84-11D0-BD3B-00A0C911CE86} as the exception message tells you. You'll notice the problem: it isn't there. Yes, the exception message is pretty lousy. The real E_NOINTERFACE is reported because the COM runtime can't find another way either, no support for IMarshal. If it would be there then you'd get to deal with the [STAThread] lie, your thread will deadlock.
That's unusual btw, COM object models that use a ThreadingModel of "Both" almost always also support marshaling. Just not for the specific one you are trying to use. DirectShow has been deprecated for the past 10 years and replaced by Media Foundation. You found one good reason why Microsoft decided to retire it.
So this is just something you need to know. A detail that isn't very different from having to know that the Random class isn't thread-safe. It is not well documented in MSDN, but as noted it is easy to discover by yourself.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With