среда, 21 мая 2014 г.

Упрощаем жизнь с COM в .NET

  В прошлой записи стало ясно, что все объекты, получаемый из COM, нужно оборачивать в IDisposable. Удалось кое-как отслеживать жизнь IEnumerator, предоставляемого COM. Если вы думаете, что на этом приключения работы COM в .NET закончились, то у меня плохие новости.

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

  1. public class ComDisposable<T>: IDisposable  
  2.     {  
  3.         protected T _comObject;  
  4.         private bool _isReleased = false;  
  5.   
  6.         internal ComDisposable(T comObject)  
  7.         {  
  8.             _comObject = comObject;  
  9.         }  
  10.   
  11.         public void Dispose()  
  12.         {  
  13.             if (_isReleased)  
  14.                 return;  
  15.   
  16.             if (_comObject != null)  
  17.                 Marshal.ReleaseComObject(_comObject);  
  18.   
  19.             _isReleased = true;  
  20.   
  21.             GC.SuppressFinalize(this);  
  22.         }  
  23.   
  24.         ~ComDisposable()  
  25.         {  
  26.             Dispose();  
  27.         }  
  28.     }  

  Использование типа предельно простое:

  1. public class COMWrapped: ComDisposable<COMNative>  
  2. {  
  3.     internal COMWrapped(COMNative comObject)  
  4.             : base(comObject)  
  5.     {  
  6.   
  7.     }  
  8.   
  9.     // Обернутые вызовы  
  10. }  

  Стало чуточку легче. Здесь и далее COMWrapped/Wrapped - свои типы-обертки, инкапсулирующие типы из обертки, сгенерированной IDE (COMNative/Native).
  Во-вторых, нас наверняка не будет устраивать то, что перечислять придется нетипизированные объекты. Хотелось бы сразу получать нужный тип при перечислении.

  1. public class COMWrapped: ComDisposable<COMNative>  
  2. {  
  3.     public COMWrapped(COMNative comObject)  
  4.             : base (comObject)  
  5.     {  
  6.   
  7.     }  
  8.   
  9.     public PropertiesCollection GetCollection()  
  10.     {  
  11.          return new PropertiesCollection(_comObject.GetCollection());  
  12.     }  
  13. }  
  14.   
  15. public class PropertiesCollection: ComDisposable<COMNativeCollection>, IEnumerable<Property>  
  16. {  
  17.     public PropertiesCollection(COMNativeCollection comObject)  
  18.             : base (comObject)  
  19.     {  
  20.   
  21.     }  
  22.   
  23.     public IEnumerator<Property> GetEnumerator()  
  24.     {  
  25.         return new PropertiesEnumerator(_comObject.GetEnumerator());  
  26.     }  
  27.   
  28.     IEnumerator IEnumerable.GetEnumerator()  
  29.     {  
  30.         return GetEnumerator();  
  31.     }  
  32. }  

  Для IEnumerator, очевидно, тоже понадобится свой тип, также требующий освобождения захваченного COM-объекта.

  1. public class PropertiesEnumerator: ComDisposable<object>, IEnumerator<Property>  
  2. {  
  3.     private IEnumerator _enum;  
  4.   
  5.     internal PropertiesEnumerator(IEnumerator enumerator)  
  6.         : base((enumerator as ICustomAdapter).GetUnderlyingObject())  
  7.     {  
  8.         _enum = enumerator;  
  9.     }  
  10.   
  11.     public Property Current  
  12.     {  
  13.         get  
  14.         {  
  15.             return new Property((COMNativeProperty)_enum.Current);  
  16.         }  
  17.     }  
  18.   
  19.     object IEnumerator.Current  
  20.     {  
  21.         get { return this.Current; }  
  22.     }  
  23.   
  24.     public bool MoveNext()  
  25.     {  
  26.         return _enum.MoveNext();  
  27.     }  
  28.   
  29.     public void Reset()  
  30.     {  
  31.         _enum.Reset();  
  32.     }  
  33. }  

  Первое, что бросается в глаза - мы параметризуем вспомогательный тип ComDisposable типом object. А в базовый конструктор отправляем подлежащий объект перечислителя.
Дело в том, что обертка, сгенерированная IDE, возвращает нам COM-объект, инкапсулированный в EnumeratorViewOfEnumVariant типе (во всяком случае для VARIANT перечислений точно). Чтобы освободить COM-перечислитель, нужно извлечь его из IEnumerator путем приведения к ICustomAdapter.
  Составим простую программу

  1. using (var obj = new COMWrapped())  
  2. {  
  3.     using (var collection = obj.GetCollection())  
  4.     {  
  5.         foreach(var property in collection)  
  6.             Console.WriteLine("{0} - {1}", property.Name, property.Value);  
  7.     }  
  8. }  

  Запускаем и получаем неосвобожденные объекты COM. Забыли про освобождение элементов перечисления. Исправим сразу в программе

  1. using (var obj = new COMWrapped())  
  2. {  
  3.     using (var collection = obj.GetCollection())  
  4.     {  
  5.         foreach(var property in collection)  
  6.         {  
  7.             Console.WriteLine("{0} - {1}", property.Name, property.Value);  
  8.             property.Dispose();  
  9.         }  
  10.     }  
  11. }  

  Теперь утечек нет.
  Интересный момент с нашим PropertyIEnumerator. Благодаря тому, что foreach и все расширяющие методы IEnumerable в BCL проверяют IEnumerator на поддержку IDisposable, у нас нет необходимости самим освобождать перечислитель. Наш IEnumerator-тип получился одноразовым.
  Вообще очень странно, что разработчики не реализовали IDisposable прямо в EnumeratorViewOfEnumVariant. Может они считали, что перечислитель не будет знать, как освобождать инкапсулированный объект? С другой стороны, это тип специально спроектирован для работы с COM. Окей, пусть и это останется на их совести.
Вернемся к использованию нашей коллекции. Нет, друзья, это не дело вызывать у каждого перечисляемого элемента Dispose. Раз реализация IEnumerator у нас подразумевается одноразовой, пусть она за всеми своими элементами сама и следит.
  Итого, нам нужен тип, производный от ComDisposable, который бы мог накапливать эти элементы, а при вызове Dispose освобождать, связанные с ними ресурсы. ComDisposable тоже придется теперь переделать. Ведь у него будут наследники, которые захотят вмешиваться в Dispose для освобождения своих объектов. Сделаем по канонам

  1. public class ComDisposable<T>: IDisposable  
  2. {  
  3.     protected T _comObject;  
  4.     private bool _isReleased = false;  
  5.   
  6.     internal ComDisposable(T comObject)  
  7.     {  
  8.         _comObject = comObject;  
  9.     }  
  10.   
  11.     public void Dispose()  
  12.     {  
  13.         Release();  
  14.         GC.SuppressFinalize(this);  
  15.     }  
  16.   
  17.     protected virtual void Release()  
  18.     {  
  19.         if (_isReleased)  
  20.             return;  
  21.   
  22.         if (_comObject != null)  
  23.             Marshal.ReleaseComObject(_comObject);  
  24.   
  25.         _isReleased = true;  
  26.     }  
  27.   
  28.     ~ComDisposable()  
  29.     {  
  30.         Release();  
  31.     }  
  32. }  
  33.   
  34. public class ComDisposableDescendants<T>: ComDisposable<T>  
  35. {  
  36.     private bool _isReleased = false;  
  37.     private HashSet<IDisposable> _forDisposing = new HashSet<IDisposable>();  
  38.   
  39.     public ComDisposableDescendants(T obj)  
  40.         : base(obj)  
  41.     {  
  42.   
  43.     }  
  44.   
  45.     protected void AddDisposable(IDisposable obj)  
  46.     {  
  47.         if (obj != null)  
  48.             _forDisposing.Add(obj);  
  49.     }  
  50.   
  51.     protected override void Release()  
  52.     {  
  53.         if (_isReleased)  
  54.             return;  
  55.   
  56.         foreach (var d in _forDisposing)  
  57.             d.Dispose();  
  58.   
  59.         _isReleased = true;  
  60.   
  61.         base.Release();  
  62.     }  
  63.   
  64.     ~ComDisposableDescendants()  
  65.     {  
  66.         Release();  
  67.     }  
  68. }  

  Ну или почти по канонам ;-)
  Виртуальный метод Release позволяет нам наследоваться и добавлять освобождение своих ресурсов. В качестве коллекции используется HashSet, так как он (набор) не допускает дублирования. По умолчанию используется reference equal компаратор, так что и тут нас все устраивает.
  Переделаем наш перечислитель под эти типы

  1. public class PropertiesEnumerator :  ComDisposableDescendants<object>, IEnumerator<Property>  
  2. {  
  3.     //...  
  4.   
  5.     public Property Current  
  6.     {  
  7.         get  
  8.         {  
  9.             var obj = new Property((COMNativeProperty)_enum.Current);  
  10.             AddDisposable(obj);  
  11.             return obj;  
  12.         }  
  13.     }  
  14.   
  15.     //...  
  16. }  

  Отлично. В нашей мини-программе, можно убрать вызовы Dispose для элементов

  1. using (var obj = new COMWrapped())  
  2. {  
  3.     using (var collection = obj.GetCollection())  
  4.     {  
  5.         foreach(var property in collection)  
  6.             Console.WriteLine("{0} - {1}", property.Name, property.Value);  
  7.     }  
  8. }  

  Все вроде работает, но давайте проведем еще пару тестов

  1. using (var obj = new COMWrapped())  
  2. {  
  3.     using (var collection = obj.GetCollection())  
  4.     {  
  5.         Console.WriteLine("Count = {0}", collection.Count());  
  6.     }  
  7. }  

  Опять неосвобожденные объекты. Надо понимать, что Count проходит по всей коллекции, но он не получает и одного элемента от перечислителя. Для того, чтобы пересчитать элементы, он вызывает только MoveNext.
  А что собственно происходит, когда мы вызываем MoveNext у EnumeratorViewOfEnumVariant? В действительности он получает очередной элемент из COM. Получается, что элемент получен внутри EnumeratorViewOfEnumVariant, но мы сами Current не вызывали. Потом, при освобождении перечислителя, все ссылки на элементы теряются. Да что там, при переборе MoveNext очередной COM-элемент затирает предыдущий. Что ж, придется эмулировать получение элемента при движении по коллекции.

  1. public class PropertiesEnumerator :  ComDisposableDescendants<object>, IEnumerator<Property>  
  2. {  
  3.     //...  
  4.   
  5.     public bool MoveNext()  
  6.     {  
  7.         var c = Current;  
  8.         return _enum.MoveNext();  
  9.     }  
  10.       
  11.     //...  
  12. }  

  Теперь все работает без утечек объектов. Но, как я писал ранее, если объектов COM, которые создают друг друга будет много, мы замучимся закрывать все их в using. Что-то надо с этим делать. В этом нам пригодится уже имеющийся ComDisposableDescendants. Пусть наш корневой тип COMWrapped сам несет ответственость за цикл жизни возвращаемой коллекции.

  1. public class COMWrapped: ComDisposableDescendants<COMNative>    
  2. {    
  3.     public COMWrapped(COMNative comObject)    
  4.             : base (comObject)    
  5.     {    
  6.     
  7.     }    
  8.     
  9.     public PropertiesCollection GetCollection()    
  10.     {  
  11.          var obj = new PropertiesCollection(_comObject.GetCollection());  
  12.          AddDisposable(obj);  
  13.          return obj;  
  14.     }    
  15. }  

  Теперь кантовать в using можно только корневой тип

  1. using (var obj = new COMWrapped())  
  2. {  
  3.     var collection = obj.GetCollection();  
  4.   
  5.     foreach(var property in collection)  
  6.         Console.WriteLine("{0} - {1}", property.Name, property.Value);  
  7. }  

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

6 комментариев:

  1. Прикольно. Возможно, будет лучше сделать класс ComDisposable internal abstract и конструктор его сделать protected. Ещё ньюанс, если получим элемент из цикла, то после выхода из цикла этот элемент, получается, будет недоступен?

    ОтветитьУдалить
    Ответы
    1. Да, у ComDisposable можно модификаторы поменять.
      Чтобы не потерять ссылку на элемент мы внутри IEnumerator.Current как раз и складываем ее в HashSet в базовом классе.

      Удалить
    2. Я имею ввиду, что, к примеру, я прохожу по элементам, чтобы найти нужные, складываю их в отдельную коллекцию. Цикл прошел, я пробую работать с тем, что сложил, а они уже диспожженные

      Удалить
    3. Это да. Тут ничего не поделаешь. Либо пиши так, чтобы потом сам диспозил элементы внутри цикла (в статье есть вариант), либо довольствуйся элементом только в пределах цикла и не парься с освобождением ресурсов.
      Есть еще вариант приводить элемент к базовым типам (var str = property.Value).
      Есть еще компромиссный вариант: добавить элементам SuppressDispose/AllowDispose, чтобы можно было управлять жизненным циклом каждого элемента отдельно.

      Удалить
    4. Я бы просто сделал public PropertiesCollection GetCollection(bool autoDisposeElements=true)

      Удалить
    5. На самом деле тут могут быть подводные камни на сервере. Например, на сервере коллекция может "держать" указатели на выдаваемые элементы. Варианта освобождения коллекции при неосвобожденном элементе тут получается два: получаем исключение от сервера, элемент освобождается принудительно. ComDisposableDescendant позволяет избежать таких ситуаций не только между коллекцией и ее элементами, но и, например, между COMWrapped и возвращаемой коллекцией.

      Удалить