Почему я не могу передать свой COM-объект интерфейсу, который он реализует на С#?

У меня есть этот интерфейс в dll (этот код показан в Visual Studio из метаданных):

#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);
    }
}

Итак, я создал COM-сервер с таким классом:

[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();
    }
}

Сервер работает нормально, и я могу использовать этот объект из VBScript. Но потом я пытаюсь использовать его у другого клиента С#:

    [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();
    }

И он терпит неудачу с

Невозможно передать COM-объект типа "System.__ ComObject" в интерфейс введите "XCapture.IDiagnostics". Эта операция завершилась неудачно, поскольку QueryInterface вызывает COM-компонент для интерфейса с IID '{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} не удалось из-за следующих Ошибка: такой интерфейс не поддерживается (исключение из HRESULT: 0x80004002 (E_NOINTERFACE)).

Что я делаю неправильно?

Я также пытался использовать InvokeMember, и этот вид работал, за исключением того, что мне не удалось получить возвращаемый ref data​​strong > .

EDIT: добавлен атрибут STAThread для моей основной процедуры. Это не решает проблему, но вы действительно должны использовать STAThread с COM, если вы не уверены, что вам это не нужно. См. Ответ Hans Passant ниже.

Ответ 1

Итак, проблема заключалась в том, что моя DLL с интерфейсом IDiagnostics была сгенерирована из TLB и что TLB никогда не регистрировался.

Поскольку DLL была импортирована из TLB, RegAsm.exe отказывается регистрировать библиотеку. Поэтому я использовал инструмент regtlibv12.exe для регистрации самого TLB:

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

Затем все волшебство начало работать.

Так как regtlibv12 не поддерживается, я до сих пор не знаю, как это сделать.

Ответ 2

Это исключение может быть проблемой DLL Hell. Но самое простое объяснение заключается в том, что отсутствует в вашем фрагменте. У вашего метода Main() отсутствует атрибут [STAThread].

Это важный атрибут, который имеет значение при использовании COM-объектов в вашем коде. Большинство из них не являются потокобезопасными, и им нужен поток, который является гостеприимным домом для кода, который не может поддерживать потоки. Атрибут заставляет состояние потока, которое вы можете явно указать с помощью Thread.SetApartmentState(). Что вы не можете сделать для основного потока приложения, так как Windows запускает его, поэтому атрибут используется для его настройки.

Если вы опустите его, то основной поток присоединяется к MTA, многопоточной квартире. Затем COM вынужден создать новый поток, чтобы предоставить компоненту безопасный дом. Для этого требуется, чтобы все вызовы были маршалированы из основного потока в этот вспомогательный поток. Ошибка E_NOINTERFACE возникает, когда COM не может найти способ сделать это, для этого требуется помощник, который знает, как сериализовать аргументы метода. Что-то, о чем должен заботиться разработчик COM, он этого не делал. Неряшливый, но не необычный.

Требование к потоку STA состоит в том, что он также перекачивает контур сообщения. Вид, который вы получаете в приложении Winforms или WPF из Application.Run(). У вас его нет в коде. Вы можете уйти от него, так как вы фактически не делаете никаких вызовов из рабочего потока. Но COM-компоненты, как правило, полагаются на цикл сообщений, чтобы быть доступными для их собственного использования. Вы заметите, что это неправильно, не поднимая события или заторможенности.

Итак, начните исправление этого, сначала применив атрибут:

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

Которая решит это исключение. Если у вас есть описанные проблемы с поднятием или блокировкой событий, вам необходимо изменить тип вашего приложения. Winforms обычно легко получить.

Я не могу иначе нанести удар по насмешливому провалу. Существуют значительные сведения о развертывании, связанные с COM, ключи реестра должны быть записаны, чтобы позволить COM обнаруживать компоненты. Вы должны получить правильные права, и интерфейсы должны быть точным соответствием. Regasm.exe требуется для регистрации .NET-компонента, который [ComVisible]. Если вы попытаетесь издеваться над существующим COM-компонентом и правильно поняли, вы уничтожите регистрацию для реального компонента. Не так уверен, что стоит преуспеть;) И у вас будет значительная проблема с добавлением ссылки на сборку [ComVisible], IDE отказывается разрешить .NET-программе использовать сборку .NET через COM. Только поздняя привязка может обмануть машину. Судя по исключению COM, вы еще не дошли до насмешки. Лучше всего использовать COM-компонент as-is, а также настоящий тест.