Как правильно уничтожать COM объект?

Обсуждение вопросов, касающихся указанной технологии.
VBTerminator
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 415
Зарегистрирован: 19.11.2008 (Ср) 20:10
Откуда: Санкт-Петербург

Как правильно уничтожать COM объект?

Сообщение VBTerminator » 22.08.2015 (Сб) 9:58

Согласно MSND, любой COM интерфейс должен быть унаследован от IUnknown. В результате классы, реализующие несколько интерфейсов, образуют ромбовидное наследование.

Также, ряд найденных мной руководств рекомендует реализовывать IUnknown::Release в программах на C++ следующим образом:

Код: Выделить всё
ULONG MyClass::Release()
{
   const ULONG newCounter = --_pointersCounter;

   if(newCounter == 0)
      delete this;

   return newCounter;
}

У меня возникло три вопроса:

  1. Как метод delete в принципе может определить, какой класс находится внизу иерархии, чтобы вызовом его деструктора грамотно освободить ресурсы? Ведь в Release() this имеет тип IUnknown*, его деструктор, как базового класса, невиртуальный, а потому всё ограничивается только его вызовом.
  2. Как производится определение базового адреса производного класса для высвобождения памяти в куче? Ведь наследование тоже невиртуальное, и, следовательно, выбранная нами копия IUnknown необязательно располагается в начале класса.
  3. Как всё-таки грамотно реализовать уничтожение COM-объекта при обнулении счётчика ссылок, чтобы не было утечки ресурсов?

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16135
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Как правильно уничтожать COM объект?

Сообщение Хакер » 22.08.2015 (Сб) 19:41

VBTerminator писал(а):Согласно MSND, любой COM интерфейс должен быть унаследован от IUnknown. В результате классы, реализующие несколько интерфейсов, образуют ромбовидное наследование.

С чего бы ради?
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

VBTerminator
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 415
Зарегистрирован: 19.11.2008 (Ср) 20:10
Откуда: Санкт-Петербург

Re: Как правильно уничтожать COM объект?

Сообщение VBTerminator » 23.08.2015 (Вс) 13:32

MSDN писал(а):You must implement IUnknown as part of every interface.

Прошу заметить, именно «every interface», не «every COM class».

Да и описание интерфейсов в заголовочных файлах, поставляемых вместе с Visual Studio (а их можно принять за эталон), имеет следующий вид:
Код: Выделить всё
MIDL_INTERFACE("b7d14566-0509-4cce-a71f-0a554233bd9b")
    IInitializeWithFile : public IUnknown
    {
        ...
    };

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16135
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Как правильно уничтожать COM объект?

Сообщение Хакер » 23.08.2015 (Вс) 18:13

Я не улавливаю, как твой ответ коррелирует с моим вопросом.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

VBTerminator
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 415
Зарегистрирован: 19.11.2008 (Ср) 20:10
Откуда: Санкт-Петербург

Re: Как правильно уничтожать COM объект?

Сообщение VBTerminator » 28.08.2015 (Пт) 8:57

А к чему тогда относится вопрос:
Хакер писал(а):С чего бы ради?


Если к ромбовидному наследованию, то в теории оно так и выходит:
Ромбовидное наследование 1.png

Ясное дело, что из-за невиртуального наследования IUnknown как общий базовый интерфейс начнёт множиться:
Ромбовидное наследование 2.png

Из-за чего, собственно, и возник второй вопрос.
У вас нет доступа для просмотра вложений в этом сообщении.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16135
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Как правильно уничтожать COM объект?

Сообщение Хакер » 28.08.2015 (Пт) 9:01

А что означают солидные и пунктирные стрелки на этой схеме?

Мы говорим о COM. COM-классы, имлементирующие несколько интерфейсов, никак с ромбовидным наследованием не стоят рядом. Повторяюсь — мы говорим о COM.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

VBTerminator
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 415
Зарегистрирован: 19.11.2008 (Ср) 20:10
Откуда: Санкт-Петербург

Re: Как правильно уничтожать COM объект?

Сообщение VBTerminator » 30.08.2015 (Вс) 15:03

Хакер писал(а):А что означают солидные и пунктирные стрелки на этой схеме?

Сплошные — наследование, пунктирные — реализация. Но разве это существенно в рамках исходных вопросов?

Хакер писал(а):Мы говорим о COM. COM-классы, имлементирующие несколько интерфейсов, никак с ромбовидным наследованием не стоят рядом. Повторяюсь — мы говорим о COM.

Тогда переформулирую: при реализации нескольких интерфейсов класс должен ещё реализовать IUnknown несколько раз, чтобы предоставить каждому из унаследованных интерфейсов возможность быть возвращённым через QueryInterface (он же возвращает только указатели на IUnknown, которые требуется приводить к типу конкретного интерфейса). Сейчас я правильно понимаю?


Вернусь к исходному вопросу. IUnknown::Release принимает в качестве this указатель на IUnknown. Как метод delete, вызываемый внутри Release, восстанавливает истинный тип производного класса, чтобы грамотно освободить ресурсы? Ведь в данном случае будет вызван деструктор только для непосредственно переданного типа (т. е. IUnknown), а это потенциальная утечка ресурсов, выделенных в производном классе. Деструктор у интерфейса имеется потому, что мы говорим о C++.

И как надо грамотно реализовать уничтожение COM-объекта, чтобы не было этой самой утечки ресурсов?

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16135
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Как правильно уничтожать COM объект?

Сообщение Хакер » 30.08.2015 (Вс) 18:32

VBTerminator писал(а):Сплошные — наследование, пунктирные — реализация.

Наследования классов в обыкновенном виде в COM нет вообще. Есть только наследование интерфейсов. Собственно говоря, голый COM и понятия «класс» не определяет. Есть объекты и всё. Объекты запросто могут быть порождены без всяких классов. Яркий пример ООП без классов — JavaScript. Так что нужно это нормально воспринимать. Объекты есть, а что такое класс... В ActiveX уже появляется понятие класса, регистрация классов, стандартизированный способ порождения экземпляров нужных классов (CoCreateInstance/GoGetClassObject/DllGetClassObject/CoRegisterClassObject/IClassFactory), механизм получения информации о классе по экземпляру класса (IProvideClassInfo). FSO — ActiveX-библиотека. Direct3D — COM-библиотека (но не ActiveX).

VBTerminator писал(а):при реализации нескольких интерфейсов класс должен ещё реализовать IUnknown несколько раз

Раз уж мы договорились, что в своём чистом виде COM не имеет понятия класса, а имеет понятие объекта, то будем избегать употребление понятия «класс». Да и про объект говорится исключительно то, что это нечто, что имеет один или более интерфейсов. И ничего более! Даже понятие «указатель на объект» с чисто академической точки зрения неверен в рамках COM (но я сам часто применяю его, в ситуации, когда не важно, на какой именно интерфейс указатель) — есть только указатели на интерфейс.

Так вот каждый интерфейс должен быть унаследован от IUnknown. Термин «унаследован» для интерфейсов означает наличие тех же методов и в том же порядке, но вовсе ничего не говорить про реализацию. Каждый должен быть унаследован от IUnknown. Это значит, что каждый должен иметь три волшебных метода.

Это значит, что на низком уровне (что такое интерфейс на низком уровне? — это некая структура в памяти неизвестной длины, у которой первый DWORD — указатель на vtable, а всё остальные подробности об интерфейсе внешний мир не знает, а знает только сам интерфейс) первые три DWORD-а в vtable должны указывать на методы с заранее предопределённым поведением. Только и всего.

Будут ли эти 3 первых указателя в vtable каждого интерфейса указывать на одни и те же процедуры или на разные — с точки зрения COM не определено и роли не играет. Главное, чтобы поведение у них было в пределах оговоренного. И пользователю объекта это тоже не должно быть важно, в норме они не проверяют это.

Поэтому твоя фраза «реализовать несколько раз» — эм... она мутная. Если её суть в том, что в коде должно быть несколько дублирующихся реализаций IUnknown — то это зависит от каждого конкретного случая.

VBTerminator писал(а):возможность быть возвращённым через QueryInterface (он же возвращает только указатели на IUnknown, которые требуется приводить к типу конкретного интерфейса)

Кто он? Если «он» — это метод QueryInterface, то неправильно. Если бы метод QueryInterface возвращал только указатели на IUnknown, в его существовании было бы 0 процентов смысла. Он возвращает указатели не на IUnknown, а именно на тот интерфейс, на который мы запросили указатель. Если бы он возвращал указатель на IUnknown, то не существовало бы способа привести этот указатель на IUnknown к другому типа (например к типу «указатель на ISomething») — ведь это самое «приведение» и делается методом QueryInterface.

VBTerminator писал(а):Вернусь к исходному вопросу. IUnknown::Release принимает в качестве this указатель на IUnknown. Как метод delete, вызываемый внутри Release, восстанавливает истинный тип производного класса, чтобы грамотно освободить ресурсы? Ведь в данном случае будет вызван деструктор только для непосредственно переданного типа (т. е. IUnknown), а это потенциальная утечка ресурсов, выделенных в производном классе. Деструктор у интерфейса имеется потому, что мы говорим о C++.

Фраза «мы говорим о С++» — оно тут самая главная. Дело в том, что ни С++ не живёт по правилам COM. Ни COM не живёт по правилам С++. Они друг в отношении друга несколько противоречивы и несовместимы. С++ плевать хотел на сам факт существоания COM. А COM не создавался так, чтобы во всём играть под дудку С++.

Самое, пожалуй, главное, что в С++ вообще нет такого понятия как «интерфейс». Что для того, чтобы хоть как-то их поиметь, в С++ используют классы с виртуальными методами.

Поэтому так или иначе, когда мы говорим о работе с COM из C++, речь идёт о том или ином наборе трюков и ухищрений, и все они — проблема самого С++, и непосредственно к COM вообще никак не относятся. Не являются все эти аспекты частью COM, а являются частью борьбы за возможность иметь COM внутри С++-мира.

Поэтому вопрос про «метод delete» (нет такого метода, есть оператор delete) можно свести к следующему:
Как код метода родительского класса может получить указатель на экземпляр дочернего класса из указателя на экземпляр родительского класса.

Это чисто проблема С++, к COM никакого отношения не имеющая (и спрашивать её следовало бы в разделе по С++).

Так вот, здесь уже ответить сходу нельзя.

Во-первых, придётся упомянуть, что в С++:
  • Сами по себе методы могут быть обычные, а могут быть виртуальными
  • В отрыве от этого: само по себе наследование может быть одиночным (один класс-предок) и множественным.
  • В отрыве от всего этого: наследование каждого класса (как в случае одиночного, так и в случае множественного наследования) может быть как не-виртуальным, так и виртуальным.

Кроме того, на правах методов: не-виртуальными и виртуальными могут быть конструкторы и десктрукторы класса (на особенно интересуют десктрукторы).Плюс в С++ классы могут перегружать операторы, и тут я напомню, что оператор delete тоже можно перегрузить (но нельзя сделать виртуальным).

Все эти вещи и их всевозможные комбинации изнутри реализовываются по своему.

И в твоём случае прежде чем отвечать на вопрос нужно понять,как именно и что наследуется.

Обычно в С++ подобие интерфейса IUnknown делается как абстрактный класс IUnknown. Абстрактный в том смысле, что все методы у него — pure virtual (и должны быть имплементированы классом-потомком). Да и вообще все COM-интерфейсы делаются абстрактными С++-классами.

А потом человек делает свой CDerivedComClass, который должен реализовывать интерфейс IUnknown. Поскольку в С++ нет интерфейсов, приходится делать это так: класс CDerivedComClass наследуется от класса IUnknown.

Тогда при реализации метода Release он будет реализован уже так:
Код: Выделить всё
int CDerivedComClass:Release()
{
    // код
}


В таком варианте поставленной проблемы нет вообще. this будет иметь типа CDerivedComClass*

Взять более сложный пример.

Пусть CDerivedComClass должен реализовать COM-интерфейс IStrangeStuff, который унаследован от IDispatch, который в свою очередь унаследован от IUnknown.

Но опять таки, в С++ нет интерфейсов как явления, поэтому все интерфейсы будут сделаны через абстрактные классы. Ни один из них не будет иметь собственной реализации метода Release. Реализовать его обязан будет класс CDerivedComClass. И это будет сделано как выше:

Код: Выделить всё
int CDerivedComClass:Release()
{
    // код
}


и внутри этого метода this будет иметь тип CDerivedComClass*, а не IUnknown*. И проблемы нет: delete this будет знать, с каким типом (каким классом) мы имеем дело и как его уничтожить.

Более того. Поскольку vtable родительского класса встраивается в vtable дочернего класса, то даже приведение указателя на класс CDerivedComClass к типу IUnknown* и последующий вызов метода Release приведёт к выполнению нужного кода.


Другое дело, если CDerivedComClass наследуется не от IUnknown, а от CSomeDefaultIUnknownImplementation. Естественно, что класс CSomeDefaultIUnknownImplementation не является частью COM, и как он работает — дело программиста. То есть тут хозяин — барин.

Понятное дело, что CSomeDefaultIUnknownImplementation наследуется от абстрактного IUnknown. А поэтому должен реализовать метод Release (в числе прочих). А потом кто угодно, чтобы не дублировать код, может свой COM-класс унаследовать от CSomeDefaultIUnknownImplementation.

Но тогда при работе метода CSomeDefaultIUnknownImplementation::Release переменная this будет иметь тип CSomeDefaultIUnknownImplementation*. Как правильно инициировать удаление дочернего объекта имея указатель на родительский объект (встроенный в дочерний)? Потенциальный ответ — виртуальные деструкторы.

Другой момент: при виртуальном множественном наследовании кастование указателя с одного класса на другой делается через vbtable (не путать с vftable). Почитать об этом можно здесь.

Но тут я выше говорил о случае, когда метод Release реализуется в каком универсальном базовом классе, от которого бы потом все дочерние COM-классы наследовались бы — ради экономии сил (чтобы не реализовывать метод Release в каждом COM-классе, а иметь универсальную реализацию). Но подход с базовым классом, реализующим Release — это только один возможный путь. Другой путь — использование шаблонов (ATL).

Но опять таки: это все проблемы самого С++. Они никак не привязаны к COM. Они и без него существуют.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

VBTerminator
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 415
Зарегистрирован: 19.11.2008 (Ср) 20:10
Откуда: Санкт-Петербург

Re: Как правильно уничтожать COM объект?

Сообщение VBTerminator » 30.08.2015 (Вс) 22:27

Хакер писал(а):... то будем избегать употребление понятия «класс».

C++, эмулируя интерфейсы классами, добавляет первым минимальную, но всё же реализацию (конструктор и деструктор по умолчанию). Именно поэтому я вынужден был смешать терминологию и обозвать COM-интерфейс C++-классом. Я этого не хотел, правда, но смешение сущностей со стороны языка вынудило.

Хакер писал(а):Объекты запросто могут быть порождены без всяких классов. Яркий пример ООП без классов — JavaScript.
Хакер писал(а):... что такое интерфейс на низком уровне? — это некая структура в памяти неизвестной длины, у которой первый DWORD — указатель на vtable ... Будут ли эти 3 первых указателя в vtable каждого интерфейса указывать на одни и те же процедуры или на разные — с точки зрения COM не определено.

Выходит, что с точки зрения COM-а интерфейс — это список методов, работающих не с любыми объектами определённого типа (как интерфейсы, к примеру, в Java), а только с одним конкретным объектом? Или понятие типа как фиксированного шаблона для создания объектов в COM тоже отсутствует как таковое?

Хакер писал(а):Другое дело, если CDerivedComClass наследуется не от IUnknown, а от CSomeDefaultIUnknownImplementation.

Нет, наследование множественное и идёт по следующей цепочке:
  • MyThumbnailProvider->IInitializeWithStream->IUnknown;
  • MyThumbnailProvider->IThumbnailProvider->IUnknown.
Реализация методов обеих IUnknown едина и расположена в MyThumbnailProvider.

Хакер писал(а):Как код метода родительского класса может получить указатель на экземпляр дочернего класса из указателя на экземпляр родительского класса.

А разве уничтожение не производится по следующему алгоритму:
  1. Пользователь какого-то реализуемого нами интерфейса вызывает его метод Release.
  2. Этому методу передаётся указатель типа IUnknown*, так как с точки зрения клиента Release объявлен только в IUnknown.
  3. Реализация Release вызывает оператор delete. И тут вопрос: this клиент передал как IUnknown*, информации о производном классе в этом указателе ноль, передать хотя бы смещение от базы производного класса до использованного при вызове Release IUnknown, негде.
    Хакер писал(а):... и внутри этого метода this будет иметь тип CDerivedComClass*, а не IUnknown*. И проблемы нет: delete this будет знать, с каким типом (каким классом) мы имеем дело и как его уничтожить.

    А как тогда будет вычислено смещение для получения CDerivedComClass из IUnknown? Нужно именно смещение, поскольку IUnknown::~IUnknown() не является виртуальным (он и не должен быть таковым, иначе он засорит собой vptr, возвращаемый в составе COM-интерфейса), и нужно вызвать напрямую CDerivedComClass::~CDerivedComClass().

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16135
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Как правильно уничтожать COM объект?

Сообщение Хакер » 31.08.2015 (Пн) 6:30

VBTerminator писал(а):Выходит, что с точки зрения COM-а интерфейс — это список методов, работающих не с любыми объектами определённого типа (как интерфейсы, к примеру, в Java), а только с одним конкретным объектом?


Так и в Джаве интерфейс — это не список методов, работающих с объектами определённого типа.

Интерфейс, что там, что тут — провозглашённый набор методов (с информацией о типах аргументов, типе возврата и как правило — с информацией о требуемом поведении метода), который может либо поддерживаться, либо не поддерживаться определённым объектом. И если объект говорит, что он поддерживает интерфейс, значит он умеет принять вызов метода и вернуть какой-то результат. Значит он имеет реализацию. Значит все объекты, которые говорят, что они поддерживают интерфейс, в одинаковой мере способны принять вызов. И тот факт, что разнородные объекты могут поддерживать один и тот же интерфейс, даёт нам возможность написания полиморфного кода. Вот и всё. Именно для этого интерфейсы и созданы.

Может быть ты имел в виду, что в Джаве для того, чтобы понять, поддерживает ли объект требуемый интерфейс, виртуальная машина обращается к информации о классе объекта и выискивает ответ там, не спрашивая сам объект. Ну, я не вникал в такие глубокие аспекты. Если это так, то да, разница есть, потому что в COM о наличии интерфейсы мы спрашиваем у самого объекта.

Собственно говоря, это первичный и естественный метод. Для ActiveX-объектов, конечно же, можно через IProvideClassInfo докопаться до ITypeInfo класса и выудить набор поддерживаемых интерфейсов. Но это нехороший путь (годится только для каких-нибудь скриптовых интерпретируемых языков, где COM-основанный ООП, и которые, при всём при том хотят работать с разными COM-интерфейсами, а не только с disp-интерфейсом (я таких языков реально существущих не знаю)) — в норме проверять наличие интерфейса и получать указатель на интерфейс надо через QueryInterface.

VBTerminator писал(а):Этому методу передаётся указатель типа IUnknown*, так как с точки зрения клиента Release объявлен только в IUnknown.

Это большое заблуждение.
Поскольку любой другой интерфейс унаследован от IUnknown, у любого другого интерфейса тоже есть метод Release. Говоря на уровне С++ с его классами это звучит так: класс IThumbnailProvider унаследован от класса IUnknown, поэтому класс IThumbnailProvider является полноправным обладателем метода Release.

Делая вызов так:
Код: Выделить всё
IInitializeWithStream* pTp;

pIws->Release()


в качестве this будет передан указатель типа IInitializeWithStream*

Внутри метода ULONG STDCALL MyThumbnailProvider::Release() указатель this будет иметь тип уже MyThumbnailProvider*.
Дальнейшие пункты (из-за неверности пункта 2) читать нет смысла.

VBTerminator писал(а):А как тогда будет вычислено смещение для получения CDerivedComClass из IUnknown?

Во-первых, как я уже выше сказал — нигде не возникает необходимости кастовать IUnknown* к CDerivedComClass*. В методе Release this будет иметь сразу правильный тип. А если бы было нужно: в момент компиляции компилятор знает, как скастовать (обычно и this аджастать не надо, если нет множественного виртуального наследования).

А «засорять vtable класса» можно только так. Если она не делится с другими классами (олицетворяющими интерфейсы), то можно делать что угодно. Если делится — то можно иметь сколько угодно своих произвольных методов в конце (после того, как кончились методы интерфейса).
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.


Вернуться в OLE / COM / ActiveX

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1

    TopList  
cron