Сообщений 4    Оценка 15 [+0/-1]         Оценить  
Система Orphus

Реализация COM-коллекций средствами C#

Автор: Сергей Иванов
JSC Dorogobuzh

Источник: RSDN Magazine #3-2005
Опубликовано: 07.10.2005
Исправлено: 10.12.2016
Версия текста: 1.0
Предисловие
Что такое COM-коллекция
Реализация коллекций
Ограничения
Реализация COM-интерфейса IEnumVARIANT
Общая схема реализации коллекций
Нетипизированная коллекция
Типизированная коллекция
Коллекция строк
Value-type коллекции
Коллекция с несколькими типами индексации
Заключение

Предисловие

Несмотря на явные преимущества управляемого кода .Net бывают ситуации, когда при построении программных систем приходится заниматься разработкой COM-объектов. Так, например, для организации пользовательских интерфейсов в системах уровня предприятия нередко используются средства Microsoft Office, функциональные возможности которых расширяются за счет макросов (программ), реализуемых с помощью VBA (Visual Basic for Applications). Возможности же VBA в свою очередь расширяются именно за счет использования COM-объектов.

ПРИМЕЧАНИЕ

Здесь имеются в виду версии Office, которые напрямую не поддерживают .Net, т.е. до версии Microsoft Office 2003 SP1.

Другая ситуация, в которой приходится использовать COM не по желанию, а по необходимости – разработка дополнительной функциональности для существующих программ, которые расширяются за счет COM. Примеры подобных программ – Microsoft Outlook, Microsoft Internet Explorer и др.

Возможны и другие ситуации, когда без COM не обойтись – например, реализация с помощью .Net существующих COM-протоколов. Частный пример – разработка OPC-серверов для систем сбора информации (OPC – OLE for Process Control).

Таким образом, хотя разработка COM-объектов и не является главной возможностью .Net, владение этой техникой может быть весьма полезным.

В настоящей статье не будет рассматриваться C++ with managed extensions – разработка COM-объектов с помощью этого языка программирования не очень существенно отличается от традиционной разработки с помощью C++. Ограничимся лишь C#, который позволяет разрабатывать COM-объекты гораздо проще, чем C++ или Delphi (субъективное мнение автора, основанное на личном практическом опыте).

Более того, не будут здесь рассматриваться ни общие вопросы COM, ни конкретные механизмы и технологии C# для разработки COM – все это достаточно подробно документировано в MSDN. Будет рассмотрен абсолютно частный вопрос, который, к сожалению, не нашел отражения ни в MSDN, ни в других источниках информации, доступных автору, включая поиск в Сети – как средствами C# реализовать COM-коллекции.

Что такое COM-коллекция

Руководство по программированию на Visual Basic для Microsoft Excel 5.0 определяет термин Automation collection следующим образом:

«A group of objects. An object's position in the collection can change whenever a change occurs in the collection. Therefore, the position of any specific object in the collection is unpredictable. This unpredictability distinguishes a collection from an array.»

Более детальное определение COM-коллекции включает следующие моменты:

  1. Доступ к коллекции, а также ее элементам, осуществляется через OLE-интерфейс. Коллекция представляет собой COM-объект.
  2. Элементами коллекции могут быть как простые данные (число, строка и пр.), так и COM-объекты. Элементы коллекции, как правило, однородны, т.е. представляют собой данные одного и того же типа. Но однородность данных не является обязательной – в общем случае элементы коллекции могут быть типа OLE-variant и содержать данные разного типа.
  3. Интерфейс коллекции должен содержать свойство Count, через которое можно получить количество элементов в коллекции.
  4. Интерфейс коллекции должен содержать метод Item, с помощью которого можно получить конкретный элемент коллекции (в некоторых руководствах, например, http://www.rsdn.ru/article/com/comcoll.xml, вместо метода рассматривается свойство с параметром). Указание элемента осуществляется через параметр метода, который соответствует некоторому логическому индексу элемента. При целочисленной индексации рекомендуется использовать нумерацию элементов коллекции с 1. Если метод Item возвращает значение типа OLE-variant, коллекция считается нетипизированной. В остальных случаях коллекция считается типизированной.
  5. Интерфейс коллекции должен содержать свойство _NewEnum, которое возвращает нумератор коллекции. Нумератор представляет собой COM-объект, реализующий интерфейс IEnumVARIANT, и обеспечивает для внешних языков программирования типа Visual Basic, VBA, VBScript и пр. выполнение циклов for each.
  6. Интерфейс коллекции может содержать другие необходимые свойства и методы, а также события. В частности, для изменяемых коллекций рекомендуется включать методы Add (добавление элементов) и Remove (удаление элементов).

С точки зрения внешнего кода (например, VBA) использовать COM-коллекции можно двумя способами.

Первый способ заключается в использовании обычного цикла for, например:

      For I = 1 To collection.Count
    Set element = collection.Item(I)
    ' некоторые действия с элементом коллекции Next I

или с учетом доступа к значению по умолчанию:

      For I = 1 To collection.Count
    Set element = collection(I)
    ' некоторые действия с элементом коллекции Next I

Второй способ заключается в использовании цикла типа for each (поддерживается не всеми языками программирования):

      For
      Each element In collection
    ' некоторые действия с элементом коллекции Next

Реализация коллекций

Ограничения

Для упрощения кода примем следующие ограничения для разрабатываемых коллекций:

  1. Будет реализован минимальный набор функциональности коллекций, т.е. коллекции будут неизменяемыми, дополнительные методы и свойства использоваться не будут.
  2. Коллекции будут использоваться только в Visual Basic, VBA, VBScript.
  3. Все коллекции будут использовать простые массивы в качестве внутреннего хранилища данных.
  4. Для всех коллекций будет использоваться единая реализация нумератора (реализация COM-интерфейса IEnumVARIANT).

Примем также единую нумерацию элементов всех коллекций, начиная с 1.

Реализация COM-интерфейса IEnumVARIANT

Общие положения

COM-интерфейс IEnumVARIANT определен в библиотеке stdole32.dll, которая находится в каталоге <Windows>\system32. Указанная библиотека сопровождается файлом stdole32.tlb, что позволяет импортировать интерфейс IEnumVARIANT в .Net с помощью утилиты tlbimp.exe из состава .Net Framework SDK. Результат импорта – сборка, аналогичная сборке stdole.dll из поставки Microsoft .NET Primary Interop Assemblies (PIA).

К сожалению, импортированный таким образом интерфейс оказывается неработоспособным при обратной передаче в COM.

PIA-сборку можно построить и вручную, но это занятие хлопотное и к тому же неблагодарное, т.к. пространство имен System.Runtime.InteropServices уже включает определение интерфейса UCOMIEnumVARIANT, соответствующего IEnumVARIANT.

Назначение и рекомендации по реализации всех методов интерфейса IEnumVARIANT хорошо документированы в MSDN. Отметим лишь, что этот интерфейс в разном программном окружении может использоваться с некоторыми вариациями. Так, Visual Basic, VBA, VBScript используют следующую схему работы с интерфейсом (псевдокод C#):

IEnumVARIANT enumerator = collection._NewEnum();
while (true)
{
  object element;
  int pceltFetched;
  int hr = enumerator.Next(1, out element, out pceltFetched);
  if (hr == 0)
    // некоторые действия с elementelsebreak;
}

Таким образом, если потенциальное использование интерфейса ограничить только указанными языками программирования, можно существенно упростить реализацию.

С целью же универсального использования различных .Net-коллекций (Array, ArrayList, etc.) можно использовать интерфейс System.Collections.IEnumerator, получаемый через интерфейс IEnumerable реальной коллекции.

С учетом изложенного общее определение класса, реализующего COM-интерфейс IEnumVARIANT, будет следующим (константы S_OK и S_FALSE будут использованы позже в реализации методов):

          public
          class _EnumVariant : UCOMIEnumVARIANT
{
  privateconstint S_OK = 0;
  privateconstint S_FALSE = 1;

  public _EnumVariant(IEnumerator enumerator)
  {
    this.enumerator = enumerator;
  }

  publicvoid Clone(int ppenum) { ... }

  publicint Next(int celt, int rgvar, int pceltFetched) { ... }

  publicint Reset(){ ... }

  publicint Skip(int celt) { ... }

  IEnumerator enumerator = null;
}

Отметим, что в классе _EnumVariant не предусматривается конструктор без параметров. Это позволяет исключить создание экземпляров нумератора во внешнем коде. Кроме того, имя класса начинается с подчеркивания, что в соответствии с рекомендациями Microsoft позволяет сделать его «невидимым» для Visual Basic, VBA, VBScript и пр.

Далее рассмотрим реализацию отдельных методов.

Метод Clone

С учетом того, что разрабатываемый нумератор будет использоваться только в контексте Visual Basic, VBA, VBScript (см. Общие положения), метод можно не реализовывать:

          public
          void Clone(int ppenum) { thrownew NotSupportedException(); }

Если же в другом контексте этот метод необходим, то реализация класса изменится принципиально, т.к. клонирование через IEnumerator невозможно.

Кроме того, в соответствии с описанием интерфейса IEnumVARIANT через параметр ppenum придется возвращать указатель на новый нумератор, а сигнатура C#-метода на первый взгляд для этого не очень подходит. Впрочем, желаемого можно добиться и в такой ситуации (соответствующие решения см. в реализации метода Next).

Метод Reset

В разрабатываемом контексте (см. Общие положения) метод Reset не является необходимым, но легко реализуется через IEnumerator:

          public
          int Reset()
{
  enumerator.Reset();
  return S_OK;
}

Единственный нюанс реализации – возврат значения S_OK. Впрочем, если бы мы описывали подобный интерфейс вручную, то получили бы аналогичную ситуацию при использовании следующей конструкции:

[PreserveSig]
int Reset();

Метод Skip

Реализация метода через IEnumerator также не представляет труда:

          public
          int Skip(int celt)
{
  for ( ; celt > 0; celt--)
    if (!enumerator.MoveNext())
      return S_FALSE;
  return S_OK;
}

Отметим только, что в реализации этого метода предполагается значение параметра celt, большее 0. Это соответствует описанию метода IEnumVARIANT::Skip в MSDN.

Метод Next

Данный метод является по сути единственным значимым и достаточно нестандартным с точки зрения реализации. Поэтому разберем его подробнее.

Сравним сигнатуры методов IEnumVARIANT.Next (OLE) и UCOMIEnumVARIANT.Next (C#):

[OLE]
HRESULT Next(
  unsignedlong      celt, 
  VARIANT FAR*       rgVar, 
  unsignedlong FAR* pCeltFetched  
);
[C#]
publicint Next(int celt, int rgvar, int pceltFetched);

Возвращаемое значение формируется аналогично методу Reset.

Параметр celt тоже не представляет проблем – в обоих случаях это обычный входной параметр.

С параметром pCeltFetched ситуация несколько сложнее. В соответствии с описанием метода IEnumVARIANT.Next через этот параметр передается указатель на переменную, в которую записывается количество считанных элементов или null, если внешнее окружение не использует эту информацию. Такая конструкция в C# не может быть определена ни через ref-, ни через out-параметр, поэтому тип этого параметра и определен как int (хотя, наверное, удобнее было бы использовать IntPtr).

Тем не менее, можно использовать класс Marshal, чтобы напрямую записать результат в неуправляемую память:

IntPtr pceltFetchedPtr = new IntPtr(pceltFetched);
if (pceltFetchedPtr != IntPtr.Zero)
  Marshal.WriteInt32(pceltFetchedPtr, 0);

Параметр rgVar создает еще большую проблему, т.к. через него внешний код передает массив (неинициализированных) значений типа OLE-variant емкостью celt, а реализация метода должна заполнить этот массив значениями считанных элементов коллекции.

Такую функциональность можно реализовать с помощью ручного маршалинга посредством класса Marshal и небольшого трюкачества с unsafe-кодом:

          int* p = (int*)rgvar;
for (int i = 0; i < celt; i++)
{
  if (enumerator.MoveNext())
    Marshal.GetNativeVariantForObject(
      enumerator.Current, 
      new IntPtr((void*)p));
  p += 4;
}

Трюк заключается в том, что для перехода к очередному элементу выходного массива нужно прибавить 4 к указателю на int,что и обеспечит смещение в 16 байт (размер OLE-variant).

Наверное, трюкачество с указателями – не самый лучший подход для управляемых языков. Впрочем, подобную технику можно встретить и в «промышленных» разработках, и даже в исходных текстах самого .Net Framework. А иначе зачем было вводить в язык возможности unsafe-кода?

Если же такие трюки кому-то не нравятся, можно описать структуру, соответствующую tagVARIANT из C++, и выполнить операции с указателем на эту структуру:

          struct VARIANT
{
  publicshort vt;
  publicshort reserved1;
  publicshort reserved2;
  publicshort reserved3;
  publicint data1;
  publicint data2;
}
//...
VARIANT* p = (VARIANT*)rgvar;
for (int i = 0; i < celt; i++, p++)
  if (enumerator.MoveNext())
    Marshal.GetNativeVariantForObject(
      enumerator.Current, 
      new IntPtr((void*)p));

Впрочем, вводить структуру только для того, чтобы выполнять корректное перемещение указателя – это уже в некотором смысле излишество. Поэтому оставим вариант с «трюком», и тогда полный текст метода Next будет выглядеть следующим образом:

          public
          int Next(int celt, int rgvar, int pceltFetched)
{
  IntPtr pceltFetchedPtr = new IntPtr(pceltFetched);
  if (pceltFetchedPtr != IntPtr.Zero)
    Marshal.WriteInt32(pceltFetchedPtr, 0);

  if (celt <= 0)
    return S_FALSE;
  else
  {
    unsafe
    {
      int* p = (int*)rgvar;
      for (int i = 0; i < celt; i++)
      {
        if (enumerator.MoveNext())
          Marshal.GetNativeVariantForObject(
            enumerator.Current, 
            new IntPtr((void*)p));
        elsereturn S_FALSE;

        p += 4; // смещаем указатель на 16 байт вперед
      }
    }

    if (pceltFetchedPtr != IntPtr.Zero)
      Marshal.WriteInt32(pceltFetchedPtr, celt);

    return S_OK;
  }
}

С учетом же того, что Visual Basic, VBA, VBScript всегда считывают по одному элементу коллекции, исходный код метода можно существенно упростить:

          public
          int Next(int celt, int rgvar, int pceltFetched)
{
  if (celt == 1 && enumerator.MoveNext())
  {
    Marshal.GetNativeVariantForObject(
      enumerator.Current, 
      new IntPtr(rgvar));
    return S_OK;
  }
  elsereturn S_FALSE;
}

Общая схема реализации коллекций

При реализации коллекций необходимо использовать рекомендации Microsoft по разработке COM-объектов в .Net, сформулированные в документации .Net Framework SDK. В частности, целесообразно использовать такие атрибуты, как GuidAttribute, ClassInterfaceAttribute, ComVisibleAttribute и пр.

Кроме того, необходимо использовать явное определение интерфейса коллекции, подобное следующему:

        public
        interface IxxxCollection
{
  [DispId(1)]
  int Count { get; }

  [DispId(0)]
  object Item([In] int index);

  [DispId(-4)]
  object _NewEnum 
  { 
    [return: MarshalAs(UnmanagedType.IUnknown)]
    get; 
  }
}

Значения идентификаторов DispId членов Item и _NewEnum важны.

Для метода Item идентификатор DispId должен иметь зарезервированное значение 0 (соответствует DISPID_VALUE), что позволяет внешнему окружению использовать этот метод неявно (пример VBA):

x = collection.Items(0)
x = collection(0)

Для свойства _NewEnum идентификатор DispId должен иметь зарезервированное значение -4 (соответствует DISPID_NEWENUM), что позволяет внешнему окружению использовать это свойство для получения нумератора. Кроме того, подчеркивание в начале названия свойства позволяет сделать это свойство «невидимым» для сред типа VB (в соответствии с рекомендациями Microsoft).

Нетипизированная коллекция

В качестве объектов нетипизированной коллекции используем COM-объекты Loan из примера, поставляемого с .Net Framework SDK (см. проект <SDK>\Samples\Technologies\Interop\Applications\LoanApps\COMtoNET\loanlib).

Исходный код:

        public
        interface ILoanCollection
{
  [DispId(1)]
  int Count { get; }

  [DispId(0)]
  object Item([In] int index);

  [DispId(-4)]
  object _NewEnum 
  { 
    [return: MarshalAs(UnmanagedType.IUnknown)]
    get; 
  }
}

publicclass LoanCollection : ILoanCollection
{
  public LoanCollection()
  {
    items = new Loan[2];
    items[0] = new Loan();
    items[0].OpeningBalance = 100;
    items[1] = new Loan();
    items[1].OpeningBalance = 200;
  }

  publicint Count
  {
    get { return items.Length; }
  }

  publicobject Item(int index)
  {
    try { return items[index - 1]; }
    catch { returnnull; }
  }

  publicobject _NewEnum 
  { 
    get { returnnew _EnumVariant(items.GetEnumerator()); }
  }

  Loan[] items;
}

Типизированная коллекция

Типизированная коллекция реализуется аналогично нетипизированной (см. выше), за исключением того, что метод Item будет возвращать типизированное значение интерфейса ILoan:

        public
        interface ITypedLoanCollection
{
  //...
  [DispId(0)]
  ILoan Item([In] int index);
  //...
}

publicclass TypedLoanCollection : ITypedLoanCollection
{
  //...public ILoan Item(int index)
  {
    try { return items[index - 1]; }
    catch { returnnull; }
  }
  //...
  Loan[] items;
}

Фактически метод Item может возвращать и объект Loan. По крайней мере, VBA одинаково корректно работает в обоих случаях.

Кроме того, в соответствии с рекомендациями Microsoft по разработке коллекций в случае, если в метод передано значение индекса несуществующего элемента, метод Item должен возвращать значение null.

Коллекция строк

Исходный код коллекции строк аналогичен коду типизированной коллекции:

        public
        interface IStringTestCollection
{
  //...
  [DispId(0)]
  string Item([In] int index);
  //...
}

publicclass StringTestCollection : IStringTestCollection
{
  //...publicstring Item(int index)
  {
    try { return items[index - 1]; }
    catch { returnnull; }
  }
  //...string[] items;
}

Отметим, что в методе Item возможен возврат null, т.к. строки допускают значение null.

Иная ситуация с коллекциями, элементы которых имеют value-тип, например, целые числа.

Value-type коллекции

Под термином «value-type коллекции» здесь понимаются коллекции, элементы которых имеют простой тип, совместимый с COM, – int, double, bool, DateTime и др. (о соответствии .Net-типов COM-типам в документации .Net Framework SDK написано достаточно подробно). К этой же категории можно отнести и коллекции, элементы которых являются структурами (структуры должны быть совместимы с COM).

Отличие value-type коллекций от коллекций объектов и строк заключается в том, что метод Item не может возвращать значение null. В случае же передачи в качестве параметра метода некорректного индекса можно сгенерировать соответствующее COM-исключение. Подобную технику, кстати, можно использовать и для коллекций объектов и строк.

Рассмотрим в качестве примера реализацию целочисленной коллекции:

        public
        interface IIntTestCollection
{
  //...
  [DispId(0)]
  int Item([In] int index);
  //...
}

publicclass IntTestCollection : IIntTestCollection
{
  //...publicint Item(int index)
  {
    return items[index - 1];
  }
  //...int[] items;
}

Отметим, что в методе Item в случае некорректного индекса сгенерируется .Net-исключение IndexOutOfRangeException, которое автоматически будет преобразовано в соответствующую COM-ошибку (80131508 – Index was outside the bounds of the array).

Коллекция с несколькими типами индексации

До сих пор рассматривались коллекции с целочисленной индексацией элементов. В общем же случае индексом коллекции может выступать произвольное значение. Так, например, коллекция Documents COM-объекта Word.Application обеспечивает доступ к своим элементам двумя способами:

Documents(1).Activate
Documents("Report.doc").Activate

Т.е. в качестве индексов элементов могут выступать как целые числа, так и строковые значения.

Построим подобную коллекцию:

        public
        interface IVariantIndexTestCollection
{
  //...
  [DispId(0)]
  int Item([In] object index);
  //...
}

publicclass VariantIndexTestCollection : IVariantIndexTestCollection
{
  //...publicint Item(object index)
  {
    string s = index asstring;

    if (s != null)
    {
      if (s == "один")
        return items[0];
      elseif (s == "два")
        return items[1];
      elsethrownew IndexOutOfRangeException();
    }
    else
    {
      try
      {
        return items[Convert.ToInt32(index) - 1];
      }
      catch (IndexOutOfRangeException e)
      {
        throw e;
      }
      catch
      {
        thrownew ArgumentException();
      }
    }
  }
  //...int[] items;
}

Здесь необходимо обратить внимание на то, что параметр index метода Item имеет тип object, который соответствует типу variant в COM. И, конечно же, из-за необходимости проверки реального типа параметра реализация метода несколько усложняется.

И еще один момент – использование класса Convert позволяет унифицировать обработку разных целочисленных типов (byte, sbyte, short, ushort, int, uint).

Заключение

Рассмотренные в настоящей статье коллекции, естественно, максимально упрощены. Тем не менее, можно надеяться, что описанные техники и приведенный в качестве примеров код помогут сделать разработку реальных COM-коллекций достаточно простой.

Полные исходные тексты прилагаются.

Исходные тексты C# реализованы как дополнение к проекту <SDK>\Samples\Technologies\Interop\Applications\LoanApps\COMtoNET\loanlib. Для сборки и регистрации тестовой библиотеки выполните следующее:

Исходные тексты примеров использования коллекций находятся в файле Samples.bas. Тестирование выполнялось в Excel 2003 (файл Test.xls), и файл Samples.bas был экспортирован из VBA. Поэтому исходный код процедур необходимо скопировать в ту среду исполнения, в которой будут выполняться прилагаемые примеры. Кроме того, если в среде исполнения требуется указывать ссылки на внешние библиотеки COM-объектов, необходимо подключить библиотеку LoanLib.dll.

В завершение хотелось бы заметить, что в статье довольно мало исходного кода. И это, наверное, хорошо, когда такие сложные задачи, как создание коллекций, решаются так просто. И все это возможно благодаря тому, что .Net обеспечивает очень мощные и в то же время простые в использовании технологии для работы с COM.


Эта статья опубликована в журнале RSDN Magazine #3-2005. Информацию о журнале можно найти здесь
    Сообщений 4    Оценка 15 [+0/-1]         Оценить