Объектная модель автоматизации Visual Studio. Интерфейсы EnvDTE




Эта статья устарела. Обновленную версию этой статьи вы можете прочитать здесь.

Аннотация

В данном разделе проводится обзор объектной модели автоматизации Visual Studio. Рассматривается общая структура модели, описывается получение доступа к её интерфейсам с помощью объектов верхнего уровня DTE/DTE2, приводятся примеры использования некоторых её элементов. Также рассматривается вопрос использования интерфейсов модели в многопоточных приложениях и приводится реализация механизмов многопоточного взаимодействия с COM интерфейсами в managed коде.

Введение

Среда разработки Visual Studio построена на принципах автоматизации и расширяемости, позволяя разработчикам интегрировать в себя практически любые новые элементы и взаимодействовать как со стандартными, так и с пользовательскими компонентами. Для реализации данных задач пользователям Visual Studio предоставлено несколько взаимно-дополняющих друг друга инструментов. Самым базовым и универсальным из которых является объектная модель автоматизации Visual Studio.

Объектная модель автоматизации представляет собой ряд библиотек, содержащих обширный хорошо структурированный набор API, покрывающих все аспекты автоматизации IDE и большинство аспектов её расширяемости. Несмотря на то, что данная модель по сравнению с другими инструментами расширения IDE не предоставляет возможностей для взаимодействия с некоторыми областями Visual Studio (а в основном это касается расширения некоторого функционала IDE), она является наиболее гибким и универсальным таким средством.

Большинство интерфейсов модели автоматизации доступны для любого вида IDE расширений, в том числе позволяя взаимодействовать с ней и из внешнего независимого процесса. Более того, сама модель может быть расширена за счёт пользовательского расширения самой Visual Studio, делая тем самым доступными новые пользовательские компоненты для других разработчиков.

Структура объектной модели автоматизации

Объектная модель Visual Studio состоит из взаимосвязанных функциональных групп объектов, охватывающих все основные аспекты среды разработки, и предоставляет возможности для их управления и расширения. Доступ к любой из этих групп возможен через глобальный интерфейс верхнего уровня DTE (Development Tools Environment). На рисунке 1 приведена общая структура объектной модели автоматизации с разделением на функциональные группы.

Рисунок 1 — Visual Studio Automation Object Model (нажмите на рисунок для увеличения)

Рисунок 1 — Visual Studio Automation Object Model (нажмите на рисунок для увеличения)

Модель может быть расширена пользователем в следующих функциональных группах:

  • проектные модели (реализация новых типов проектов, поддержка новых языков);
  • документные модели (реализация новых типов документов и редакторов документов);
  • модели уровня редактора кода (реализация поддержка специфичных конструкций для новых языков);
  • модели сборочного процесса проектов.

Расширение модели автоматизации доступно только для модулей VSPackage.

Стоит помнить, что, несмотря на универсальность модели автоматизации, не все из представленных здесь групп интерфейсов могут быть одинаково использованы в расширениях различного типа. В частности, некоторые возможности модели будут недоступны для внешних процессов, т.к. они завязаны на такие специфичные типы модулей-расширений, как Add-In или VSPackage. Поэтому выбирая тип будущего разрабатываемого модуля, следует ориентироваться прежде всего на требуемый от него функционал.

Получение ссылок на объекты DTE/DTE2

Для создания приложения автоматизации Visual Studio прежде всего необходимо получить доступ непосредственно к самим объектам автоматизации. Для этого необходимо, во-первых, подключить правильные версии библиотек, содержащих необходимые managed обёртки для API среды в пространстве имён EnvDTE. Во-вторых, нужно получить ссылку на главный объект верхнего уровня модели автоматизации — интерфейс DTE2.

В процессе развития среды Visual Studio некоторые из объектов автоматизации претерпевали изменения и получали дополнительную функциональность. Для сохранения обратной совместимости с уже существовавшими модулями-расширениями, вместо обновления старых интерфейсов EnvDTE были созданы новые пространства имён EnvDTE80, EnvDTE90, EnvDTE100 и т.п. Большинство подобных новых интерфейсов имеют такие же имена, что и в EnvDTE, но с добавлением на конце порядкового номера, например, Solution и Solution2. Рекомендуется использовать новые версии интерфейсов при создании нового проекта, поскольку именно они содержат наиболее полную функциональность. Стоит заметить, что поля и методы интерфейса DTE2 возвращают объекты, типы которых соответствуют интерфейсу DTE, т.е. при обращении к dte2.Solution возвращается Solution, а не Solution2, как может показаться.

Несмотря на то, что новые пространства имён EnvDTE80, EnvDTE90, EnvDTE100 содержат некоторую новую функциональность, именно в EnvDTE всё еще находится основная часть объектов автоматизации. Поэтому, чтобы иметь доступ ко всем существующем интерфейсам, необходимо подключить к проекту все версии managed библиотек-обёрток COM интерфейсов, а также получить ссылки как на DTE, так и на DTE2.

Способ получения ссылки на верхний объект EnvDTE зависит от типа разрабатываемого расширения Visual Studio. Далее рассмотрим 3 вида расширений: Add-In, VSPackage и независимый от MSVS внешний процесс.

Add-In расширение

В случае разработки модуля вида Add-In, доступ к DTE интерфейсу можно получить в методе OnConnection, который должен быть реализован для интерфейса IDTExtensibility, предоставляющего доступ к событиям взаимодействия среды и Add-In модулей. Метод OnConncetion вызывается в момент загрузки модуля в IDE, что может произойти как непосредственно в момент загрузки среды, так и после первого обращения к нему. Получение ссылки выглядит следующим образом:

public void OnConnection(object application,
  ext_ConnectMode connectMode, object addInInst, ref Array custom)
           {
               _dte2 = (DTE2)application;
               ...
           }

Add-In модуля может быть инициализирован как в момент загрузки самой среды, так и после первого к нему обращения в текущем сеансе работы IDE. Поэтому, для определения момента загрузки в методе OnConnection можно использовать параметр connectMode.

switch(connectMode)
{
    case ext_ConnectMode.ext_cm_UISetup:
        ...
        break;

    case ext_ConnectMode.ext_cm_Startup:
        ...
        break;

    case ext_ConnectMode.ext_cm_AfterStartup:
        ...
        break;

    case ext_ConnectMode.ext_cm_CommandLine:
        ...
        break;
}

Как видно из примера, Add-in может быть загружен одновременно с загрузкой самой IDE (установлен как startup в Add-In manager), при первом обращении к нему, либо при вызове его из командной строки. Вариант ext_ConnectMode.ext_cm_UISetup вызывается только 1 раз за время существования плагина, при его самой первой загрузке. Этот вариант предназначен для инициализации интегрируемых в среду UI элементов (что будет рассмотрено подробнее позже).

Если Add-In загружается при старте Visual Studio (ext_ConnectMode.ext_cm_Startup), то в момент получения управления методом OnConnect среда может ещё быть не полностью проинициализирована. В таком случае инициализацию непосредственно DTE интерфейса предпочтительно отложить до окончания инициализации самой среды. Для этого можно воспользоваться предоставляемым интерфейсом IDTExtensibility обработчиком OnStartupComplete.

public void OnStartupComplete(ref Array custom)
{
    ...
}

VSPackage расширение

Для расширения типа VSPackage интерфейс DTE может быть получен как глобальный сервис Visual Studio с помощью метода GetService класса Package:

DTE dte = MyPackage.GetService(typeof(DTE)) as DTE;

Обратите внимание, что метод GetService может возвращать и null в случае, если Visual Studio не была полностью загружена и инициализирована, то есть находится в так называемом "zombie" состоянии. Для корректной обработки данной ситуации можно отложить получение ссылки на интерфейс DTE до момента, пока она не потребуется. Если же доступ к ссылке на DTE нужен непосредственно внутри метода Initialize, то можно воспользоваться интерфейсом IVsShellPropertyEvents (унаследовав от него наш подкласс Package), и тогда получить ссылку в обработчике OnShellPropertyChange.

DTE dte;
uint cookie;
 
protected override void Initialize()
{
  base.Initialize();

  IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;
  if (shellService != null)
    ErrorHandler.ThrowOnFailure(
      shellService.AdviseShellPropertyChanges(this,out cookie));
...
}
 
public int OnShellPropertyChange(int propid, object var)
{
  // when zombie state changes to false, finish package initialization
  if ((int)__VSSPROPID.VSSPROPID_Zombie == propid)
  {
    if ((bool)var == false)
    {
     this.dte = GetService(typeof(SDTE)) as DTE;
     IVsShell shellService = GetService(typeof(SVsShell)) as IVsShell;

     if (shellService != null)
       ErrorHandler.ThrowOnFailure(
         shellService.UnadviseShellPropertyChanges(this.cookie) );
     this.cookie = 0;
    }
  }
  return VSConstants.S_OK;
}

Следует обратить внимание на то, что в разных версиях Visual Studio процесс инициализации VSPackage модуля на старте IDE может происходить по разному. Так в VS2005 и VS2008, благодаря их относительно быстрой загрузке, попытка доступа к DTE будет гарантированно возвращать null. В Visual Studio 2010 же может сложиться впечатление, что в Initialize() уже можно получить к DTE доступ. Однако данное впечатление обманчиво, поскольку такое использование DTE приводит к появлению "плавающих" трудно отлавливаемых ошибок, а сам DTE оказывается не полностью инициализированнымв момент получения ссылки. Поэтому не стоит пренебрегать описанным выше универсальным методом обработки состояния загрузки IDE для всех версий Visual Studio.

Независимый внешний процесс

DTE является верхне-уровневой абстракцией для самой среды Visual Studio в модели автоматизации. Для получения ссылки на него из внешнего приложения нужно использовать его ProgID COM идентификатор, например VisualStudio.DTE.10.0 для Visual Studio 2010. Приведём пример инициализации нового экземпляра IDE и получения ссылки на её DTE интерфейс.

// Get the ProgID for DTE 8.0.
System.Type t = System.Type.GetTypeFromProgID(
    "VisualStudio.DTE.10.0", true);
// Create a new instance of the IDE.
object obj = System.Activator.CreateInstance(t, true);
// Cast the instance to DTE2 and assign to variable dte.
EnvDTE80.DTE2 dte = (EnvDTE80.DTE2)obj;
// Show IDE Main Window
dte.MainWindow.Activate();

В поведённом примере мы фактически создаём новый объект DTE, запуская процесс devenv.exe методом CreateInstance. При этом GUI окно среды будет отображено только после вызова метода Activate. Простейший же способ получить ссылку на DTE из уже запущенного экземпляра Visual Studio будет выглядеть так:

EnvDTE80.DTE2 dte2;
dte2 = (EnvDTE80.DTE2)
  System.Runtime.InteropServices.Marshal.GetActiveObject(
    "VisualStudio.DTE.10.0");

Однако если было запущено несколько экземпляров IDE, то метод GetActiveObject вернёт ссылку только на интерфейс самого первого из запущенных экземпляра среды. Рассмотрим один из возможных вариантов получения DTE интерфейса у запущенного экземпляра Visual Studio по PID его процесса.

using EnvDTE80;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[DllImport("ole32.dll")]
private static extern void CreateBindCtx(int reserved, 
                                         out IBindCtx ppbc);
[DllImport("ole32.dll")]
private static extern void GetRunningObjectTable(int reserved, 
  out IRunningObjectTable prot);

public static DTE2 GetByID(int ID)
{
  //rot entry for visual studio running under current process.
  string rotEntry = String.Format("!VisualStudio.DTE.10.0:{0}", ID);
  IRunningObjectTable rot;
  GetRunningObjectTable(0, out rot);
  IEnumMoniker enumMoniker;
  rot.EnumRunning(out enumMoniker);
  enumMoniker.Reset();
  IntPtr fetched = IntPtr.Zero;
  IMoniker[] moniker = new IMoniker[1];
  while (enumMoniker.Next(1, moniker, fetched) == 0)
  {
    IBindCtx bindCtx;
    CreateBindCtx(0, out bindCtx);
    string displayName;
    moniker[0].GetDisplayName(bindCtx, null, out displayName);
    if (displayName == rotEntry)
    {
      object comObject;
      rot.GetObject(moniker[0], out comObject);
      return (EnvDTE80.DTE2)comObject;
    }
  }
  return null;
}

Здесь мы получили интерфейс DTE, идентифицировав нужный нам экземпляр IDE в таблице запущенных COM объектов (ROT, Running Object Table) по его идентификатору. Теперь мы можем получить DTE для любого из запущенных экземпляров Visual Studio, например:

Process Devenv;
...
//Get DTE by Process ID
EnvDTE80.DTE2 dte2 = GetByID(Devenv.Id);

Дополнительно, для получения проектно-специфичных интерфейсов (в том числе и пользовательских расширений модели), например для проектной модели CSharpProjects, через имеющийся DTE объект, можно воспользоваться методом GetObject:

Projects projects = (Projects)dte.GetObject("CSharpProjects");

Метод GetObject вернёт нам множество Projects стандартных Project объектов, каждый из которых будет, помимо общих в модели автоматизации свойств, содержать ссылку и на специфичный для данной модели тип.

Документы текстового редактора Visual Studio

Модель автоматизации определяет текстовые документы Visual Studio через интерфейс текстового документа TextDocument. Исходные C/C++ файлы открываются средой как текстовые документы. TextDocument основан на общем интерфейсе документов модели автоматизации (интерфейс Document), описывающем любой открытый в редакторе или дизайнере Visual Studio файл. Ссылку на объект текстового документа можно получить через поле Object объекта Document. Получим, например, текстовый документ для активного (т.е. открытого и имеющего фокус) документа из текстового редактора IDE.

EnvDTE.TextDocument objTextDoc =
(TextDocument)PVSStudio.DTE.ActiveDocument.Object("TextDocument");

Редактирование документов

Интерфейс TextSelection позволяет модифицировать текст и управлять выделениями. Методы данного интерфейса отражают функционал текстового редактора Visual Studio, т.е. позволяют работать непосредственно с видимым в UI текстом.

EnvDTE.TextDocument Doc =
  (TextDocument)PVSStudio.DTE.ActiveDocument.Object(string.Empty);
Doc.Selection.SelectLine();
TextSelection Sel = Doc.Selection;
int CurLine = Sel.TopPoint.Line;
String Text = Sel.Text;
Sel.Insert("test\r\n");

В данном примере мы выделяем строку текста под курсором, считываем выделенные текст и заменяем его на строку 'test'.

Также интерфейс TextDocument позволяет редактировать текст документа с помощью интерфейса EditPoint. Данный интерфейс схож с интерфейсом TextSelection, но в отличие от него позволяет манипулировать данными текстового буфера, а не текстом, отображённым непосредственно в редакторе. Отличие в том, что на текстовый буфер не влияют такие параметры редактора, как WordWrap и Virtual Spaces. Заметим, что для данных методов редактирования не доступны участки текста из read-only блоков.

Приведём пример редактирования с помощью EditPoint, в котором текст добавляется в конце текущей строки редактирования (строки с курсором).

objEditPt = objTextDoc.StartPoint.CreateEditPoint();
int lineNumber = objTextDoc.Selection.CurrentLine;
objEditPt.LineDown(lineNumber - 1);

EditPoint objEditPt2 = objTextDoc.StartPoint.CreateEditPoint();
objEditPt2.LineDown(lineNumber - 1);
objEditPt2.CharRight(objEditPt2.LineLength);

String line = objEditPt.GetText(objEditPt.LineLength);
String newLine = line + "test";
objEditPt.ReplaceText(objEditPt2, newLine,
  (int)vsEPReplaceTextOptions.vsEPReplaceTextKeepMarkers);

Навигация по документам

Для модулей типа VSPackage доступен ряд глобальных служб среды, позволяющих открывать и просматривать документы. Данные службы можно получить с помощью метода Package.GetGlobalService(). Заметим, что используемые здесь интерфейсы не являются частью универсальной модели DTE, доступны только для объекта пакета расширения Package и поэтому не могут быть использованы в других видах расширений Visual Studio. Тем не менее, эти службы удобны для использования при работе с IDE документами совместно с описанным выше интерфейсом Document, поэтому мы остановимся на них более подробно в данном разделе.

Интерфейс IVsUIShellOpenDocument позволяет контролировать состояние открытых документов среды. Приведём пример открытия документа через путь до связанного с ним файла.

String path = "C:\Test\test.cpp";
IVsUIShellOpenDocument openDoc =
  Package.GetGlobalService(typeof(IVsUIShellOpenDocument))
    as IVsUIShellOpenDocument;

IVsWindowFrame frame;
Microsoft.VisualStudio.OLE.Interop.IServiceProvider sp;
IVsUIHierarchy hier;
uint itemid;
Guid logicalView = VSConstants.LOGVIEWID_Code;
if (ErrorHandler.Failed(
  openDoc.OpenDocumentViaProject(path, ref logicalView, out sp, 
    out hier, out itemid, out frame))
      || frame == null)
{
  return;
}
object docData;
frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out docData);

Файл либо будет открыт в новом редакторе, либо его редактор получит фокус, если он уже был открыт ранее. Далее получим из открытого документа текстовый буфер типа VsTextBuffer.

// Get the VsTextBuffer  
VsTextBuffer buffer = docData as VsTextBuffer;
if (buffer == null)
{
  IVsTextBufferProvider bufferProvider = docData as
    IVsTextBufferProvider;
  if (bufferProvider != null)
  {
    IVsTextLines lines;
    ErrorHandler.ThrowOnFailure(bufferProvider.GetTextBuffer(
      out lines));
    buffer = lines as VsTextBuffer;
    Debug.Assert(buffer != null, 
      "IVsTextLines does not implement IVsTextBuffer");
    if (buffer == null)
    {
      return;
    }
  }
} 

Интерфейс IVsTextManager позволяет управлять всеми текстовыми буферами, активными в среде. Например, можно произвести навигацию в текстовом документе с помощью метода текстового менеджера NavigateToLineAndColumn в полученном выше буфере:

IVsTextManager mgr =
  Package.GetGlobalService(typeof(VsTextManagerClass))
  as IVsTextManager;
mgr.NavigateToLineAndColumn(buffer, ref logicalView, line, 
  column, line, column);

Подписка и обработка событий

События объектов автоматизации определены в корневом элементе DTE.Events. При этом данный интерфейс содержит ссылки как на стандартные (общие) события IDE (CommandEvents, SolutionEvents), так и на события различных элементов среды (отдельные типы проектов, редакторы, инструменты и т.п.), в том числе и определённых пользователем. Для получения ссылки для подобного объекта автоматизации можно использовать метод GetObject.

При подписке на события DTE стоит помнить, что в момент инициализации расширения данный интерфейс может быть ещё не доступен. Поэтому важно помнить и учитывать порядок инициализации вашего разрабатываемого модуля в случае, если подписка на DTE.Events потребуется именно в момент инициализации плагина-расширения. При этом правильная обработка этого момента для получения ссылки на DTE будет выглядеть по-разному для разных версий расширений, как было описано в предыдущем разделе.

Получим ссылку для событий проектной модели Visual C++, определённой интерфейсом VCProjectEngineEvents и назначим обработчик на удаление элемента из Solution дерева:

VCProjectEngineEvents m_ProjectItemsEvents =
  PVSStudio.DTE.Events.GetObject("VCProjectEngineEventsObject") 
    as VCProjectEngineEvents;
m_ProjectItemsEvents.ItemRemoved +=
  new _dispVCProjectEngineEvents_ItemRemovedEventHandler(
    m_ProjectItemsEvents_ItemRemoved);

События MDI окон

Для обработки стандартных событий MDI окна среды можно воспользоваться интерфейсом Events.WindowEvents. Данный интерфейс позволяет назначить как отдельный обработчик для окна (определённого через интерфейс EnvDTE.Window), так и общий обработчик для всех окон среды.

Назначение общего обработчика для среды на переключение между окнами:

WindowEvents WE = PVSStudio.DTE.Events.WindowEvents;
WE.WindowActivated += 
  new _dispWindowEvents_WindowActivatedEventHandler(
    Package.WE_WindowActivated);

Назначение обработчика на переключение для активного в данный момент окна возможно через индексатор свойства WindowEvents:

WindowEvents WE =
  m_dte.Events.WindowEvents[MyPackage.DTE.ActiveWindow];
WE.WindowActivated += new
  _dispWindowEvents_WindowActivatedEventHandler(
    MyPackage.WE_WindowActivated);

События IDE команд

Непосредственно работа с командами и их расширение через модель автоматизации рассматриваются в отдельном разделе. Здесь мы затронем вопрос обработки событий команд (но не самого выполнения команд). Назначение обработчиков на события возможно с помощью интерфейса Events.CommandEvents. Свойство CommandEvents, по аналогии с обработкой событий MDI окон, также позволяет назначить обработчик как для всех команд IDE, так и для конкретной команды с помощью индексатора.

Назначение обработчика команд среды на событие завершения выполнения команды:

CommandEvents CEvents = DTE.Events.CommandEvents;
CEvents.AfterExecute += new
  _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

Для назначения обработчика конкретной команды необходимо сначала идентифицировать эту команду. Каждая команда среды идентифицируется парой GUID:ID, причём для пользовательских команд эти значения задаются непосредственно при интеграции команды разработчиком, например через таблицы команд (VSCT). Visual Studio имеет специальный отладочный режим работы, позволяющий узнать идентификаторы любой команды. Для активации этого режима нужно добавить следующий ключ в системный реестр (пример для Visual Studio 2010):

[HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\General]
"EnableVSIPLogging"=dword:00000001

После перезапуска среды с этим ключом, наведение курсора мыши (иногда не срабатывает сразу, если не кликнуть) с зажатыми клавишами CTRL+SHIFT вызывает диалоговое окно, в котором приведены все внутренние идентификаторы команды. Из них нас интересуют значения Guid и CmdID. Пример назначения обработчика для команды File.NewFile:

CommandEvents CEvents = DTE.Events.CommandEvents[
  "{5EFC7975-14BC-11CF-9B2B-00AA00573819}", 221];
CEvents.AfterExecute += new  
  _dispCommandEvents_AfterExecuteEventHandler(C_AfterExecute);

Обработчик, назначенный подобным образом, получает управление после выполнения команды.

void C_AfterExecute(string Guid, int ID, object CustomIn, 
  object CustomOut)
{
  ...
}

Не стоит путать этот обработчик (после завершению выполнения) с непосредственным обработчиком выполнения самой команды, который может быть назначен при её инициализации (из модуля-расширения в случае, если команда является пользовательской). Подробно работа с командами среды будет описана в специальном посвященном IDE командам разделе.

В заключение данного подраздела заметим, что при разработке пакета-расширения (VSPackage) PVS-Studio мы столкнулись с необходимостью хранить ссылки на объекты интерфейса, содержащие в свою очередь наши делегаты-обработчики (такие, как CommandEvents, WindowEvents и прочие), в качестве полей нашего главного подкласса Package. При назначении же обработчика через локальную переменную, определённую внутри самой функции, данный обработчик терялся сразу после выхода из неё. Похоже, что это происходит в результате действий сборщика мусора .NET. Причём происходит это несмотря на то, что мы получаем ссылку на такой объект из интерфейса DTE, однозначно существующего в течение всего времени жизни нашего модуля-расширения.

Взаимодействие с COM интерфейсами DTE2 в многопоточном приложении

Изначально пакет расширения PVS-Studio не содержал в себе каких-то особенных механизмов для обеспечения потоковой безопасности при работе с Visual Studio API. При этом мы старались ограничить всё взаимодействие с данными интерфейсами в рамках одного, создаваемого нашим плагином, фонового потока. Данный подход работал без видимых проблем долгое время. Однако сообщения о схожих ComException ошибках от нескольких наших пользователей побудили нас более детально разобраться в данном вопросе и реализовать собственный механизм для обеспечения потоковой безопасности в COM Interop.

Хотя объектная модель автоматизации Visual Studio не является потоково-безопасной, она предоставляет возможности для взаимодействия с многопоточными приложениями. Приложение Visual Studio является COM (Component Object Mode) сервером. Для обращения COM-клиентов (в данном случае это наш модуль-расширение) к потоково-небезопасным серверам технология COM предоставляет механизм, известный как STA (single-threaded apartment) модель. В терминах COM Apartment представляет собой логический контейнер внутри процесса, в котором объекты и потоки имеют общие права межпотокового доступа. STA позволяет содержать в таком контейнере только один поток, но неограниченное количество объектов. Обращения же из других потоков к таким потоково-небезопасным объектам в STA преобразуются в сообщения и помещаются в очередь сообщений. Затем сообщения поочерёдно достаются из этой очереди и преобразуются в вызовы методов потоком, находящимся в STA, что делает возможным обращение к этим небезопасным объектам на сервере только из одного потока.

Использование Apartment механизма в managed коде

Непосредственно .NET Framework не использует Apartment механизм COM. Поэтому, когда в ситуации COM взаимодействия managed приложение обращается к COM объекту, CLR (Common Language Runtime) должен создать и инициализировать apartment контейнер. Managed поток может создать и войти как в MTA (multi-threaded apartment, контейнер, который, в противовес STA, может содержать несколько потоков одновременно), так и в STA, причём, по умолчанию, поток будет запущен именно в MTA. Задать apartment можно с помощью метода Thread.SetApartmentState перед непосредственным запуском потока:

Thread t = new Thread(ThreadProc);
t.SetApartmentState(ApartmentState.STA);
...
t.Start();

Т.к. apartment не может быть изменён для уже запущенного потока, для его установки в основном потоке в managed приложения в STA необходимо воспользоваться атрибутом STAThread:

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

Реализация фильтра ошибок доступа к COM интерфейсам в managed среде

Так как в STA все обращения к COM серверу сериализуются, один из вызывающих клиентов может быть блокирован или отклонён в моменты, когда сервер занят, обрабатывает другие обращения или другой поток уже находится в apartment контейнере. В случае, когда COM сервер отклоняет обращение клиента, .NET COM Interop генерирует исключения вида System.Runtime.InteropServices.COMException ("The message filter indicated that the application is busy").

В случае разработки встраиваемого в Visual Studio модуля (VSPackage, Add-In) или макроса, управление в него передаётся обычно из главного STA UI потока среды (перехват event'ов, обработка изменений состояний и т.п.). Обращение к COM интерфейсам автоматизации из этого основного потока является безопасным. Однако если планируется создание других фоновых потоков и обращение к интерфейсам EnvDTE из них (например, длительные вычисления, которые могут привести к зависанию интерфейса среды), то желательно реализовать механизм для обработки отклонённых сервером вызовов.

Наиболее часто с подобными COM Exception ошибками мы сталкивались при работе с PVS-Studio в окружении других установленных в Visual Studio плагинов при одновременном взаимодействием самого пользователя с интерфейсом IDE. Вполне закономерно, что подобная ситуация приводила к одновременным параллельным запросам объектов, находящихся в STA, и соответственно, отклонению части из этих запросов.

Для выборочной обработки входящих и исходящих сообщений COM предоставляет интерфейс IMessageFilter. Если сервер реализует его, то все запросы поступают в метод HandleIncomingCall, а клиент информируется об отклонённом запросе через метод RetryRejectedCall. При этом появляется возможность либо повторить запрос, либо корректно отработать отказ на него (например, показав пользователю соответствующий диалог о занятости сервера). Далее приведём пример реализации обработки отклонённого запроса в managed приложении.

[ComImport()]
[Guid("00000016-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IMessageFilter
{
  [PreserveSig]
  int HandleInComingCall(
    int dwCallType,
    IntPtr hTaskCaller,
    int dwTickCount,
    IntPtr lpInterfaceInfo);

  [PreserveSig]
  int RetryRejectedCall(
    IntPtr hTaskCallee,
    int dwTickCount,
    int dwRejectType);

  [PreserveSig]
  int MessagePending(
    IntPtr hTaskCallee,
    int dwTickCount,
    int dwPendingType);
}

class MessageFilter : MarshalByRefObject, IDisposable, IMessageFilter
{

  [DllImport("ole32.dll")]
  [PreserveSig]
  private static extern int CoRegisterMessageFilter(
    IMessageFilter lpMessageFilter, 
    out IMessageFilter lplpMessageFilter);

  private IMessageFilter oldFilter;
  private const int SERVERCALL_ISHANDLED = 0;
  private const int PENDINGMSG_WAITNOPROCESS = 2;
  private const int SERVERCALL_RETRYLATER = 2;

  public MessageFilter()
  {
    //Starting IMessageFilter for COM objects
    int hr =
      MessageFilter.CoRegisterMessageFilter(
        (IMessageFilter)this, 
         out this.oldFilter);
    System.Diagnostics.Debug.Assert(hr >= 0, 
      "Registering COM IMessageFilter failed!");
  }

  public void Dispose()
  {
    //disabling IMessageFilter
    IMessageFilter dummy;
    int hr = MessageFilter.CoRegisterMessageFilter(this.oldFilter, 
                                                   out dummy);
    System.Diagnostics.Debug.Assert(hr >= 0, 
      "De-Registering COM IMessageFilter failed!")
    System.GC.SuppressFinalize(this);
  }

  int IMessageFilter.HandleInComingCall(int dwCallType, 
    IntPtr threadIdCaller, int dwTickCount, IntPtr lpInterfaceInfo)
  {
    // Return the ole default (don't let the call through).
    return MessageFilter.SERVERCALL_ISHANDLED;
  }

  int IMessageFilter.RetryRejectedCall(IntPtr threadIDCallee, 
    int dwTickCount, int dwRejectType)
  {
    if (dwRejectType == MessageFilter.SERVERCALL_RETRYLATER)
    {
      // Retry the thread call immediately if return >=0 & 
      // <100.
      return 150; //waiting 150 mseconds until retry
    }
    // Too busy; cancel call. SERVERCALL_REJECTED
    return -1;
    //Call was rejected by callee. 
    //(Exception from HRESULT: 0x80010001 (RPC_E_CALL_REJECTED))
  }

  int IMessageFilter.MessagePending(
      IntPtr threadIDCallee, int dwTickCount, int dwPendingType)
  {
    // Perform default processing.
    return MessageFilter.PENDINGMSG_WAITNOPROCESS;
  }
}

Теперь мы сможем использовать MessageFilter при работе с COM интерфейсами из фонового потока:

using (new MessageFilter())
{
  //COM-interface dependent code
  ...
}

Рекомендуемые ссылки

Другие статьи этого цикла



Найдите ошибки в своем C, C++, C# и Java коде

Предлагаем попробовать проверить код вашего проекта с помощью анализатора кода PVS-Studio. Одна найденная в нём ошибка скажет вам о пользе методологии статического анализа кода больше, чем десяток статей.

goto PVS-Studio;


Найденные ошибки

Проверено проектов
363
Собрано ошибок
13 495

А ты совершаешь ошибки в коде?

Проверь с помощью
PVS-Studio

Статический анализ
кода для C, C++, C#
и Java

goto PVS-Studio;