Расширение интерфейсов MIDL и дизайн COM-объектов
Я читал о различных шаблонах проектирования COM, подробно описанных в Поваренной книге программиста COM, а также о некоторых связанных с ним потоках SO, в частности, обсуждении композиции против множественного наследования. Возможно, из-за того, что я слишком новичок в C++ и COM, возможно, мне не хватает замечаний, высказанных в разных источниках, поэтому вот мой вопрос, выраженный в одном предложении:
Могу ли я расширить интерфейс, сгенерированный MIDL, для внутреннего использования DLL, и если да, то как правильно обрабатывать проблему алмаза / параллельную иерархию с учетом ограничения MIDL/COM?
Грязные детали...
Надеюсь, чтобы помочь другим определить, где моё замешательство, вот мои предположения:
1) COM не поддерживает виртуальное наследование и допускает множественное наследование только через интерфейсы.
2) Несмотря на то, что COM его не видит, для меня не должно быть противозаконным использование неподдерживаемого наследования C++, если я не ожидаю, что это будет открыто для COM.
3) Поскольку MIDL допускает только одиночное наследование интерфейсов, если у меня параллельная иерархия, мне нужно объединить их для кокласса.
4) MIDL, по-видимому, не объявляет сам кокласс, поэтому мне нужно было бы написать.h файл, объявляющий фактический класс, и я могу расширять его по мере необходимости, понимая, что потребители COM не могут его использовать (и это нормально).
То, что я хочу сделать, - это иметь базовый объект (я еще не решил, будет ли он абстрактным или нет, хотя я думаю, что это будет сейчас), который обрабатывает большинство деталей реализации и делегирует некоторые специфические функциональные возможности подклассам. Клиент обычно использует подклассы. Так,
project.idl
import "oaidl.idl"
import "ocidl.idl"
[
object,
uuid(...),
dual,
oleautomation
]
interface IBase : IDispatch {
//stuff I want to show to COM
};
[
object,
uuid(...),
dual,
oleautomation
]
interface IChild1 : IBase {
//stuff (in addition to base) I want to show to COM
};
[
object,
uuid(...),
dual,
oleautomation
]
interface IChild2 : IBase {
//stuff (in addition to base) I want to show to COM
};
[
uuid(...),
version(...),
]
library myproject {
importlib("stdole32.tlb");
interface IBase;
interface IChild1;
interface IChild2;
[
uuid(...),
]
coclass Base {
[default]interface IBase;
interface IOle*; //include other IOle* interfaces required for the functionality
};
[
uuid(...),
]
coclass Child1 {
[default]interface IChild1;
interface IOle*; //those are delegated to the base members
};
[
uuid(...),
]
coclass Child2 {
[default]interface IChild2;
interface IOle*; //those are delegated to the base members
};
};
base.h
#include base_h.h //interfaces generated by MIDL
// I assume I need to re-include IOle* because IBase has no relationship
// and C++ wouldn't know that I want the object base to also have those
// interfaces...
class base : public IBase,
public IOle* {
//handle all IUnknown, IDispatch and other IOle* stuff here
//as well as the common implementations as appropriate
};
child1.h
#include base.h
//I'm not sure if I need to re-include the IOle* interfaces...
//I also assume that by inheriting base, child1 also inherits its interface
class Child1 : public Base,
public IChild1 {
//specific details only, let base handle everything else.
};
child2.h
#include base.h
//I'm not sure if I need to re-include the IOle* interfaces...
class Child2 : public Base,
public IChild2 {
//specific details only, let base handle everything else.
};
Концептуально, создание нового дочернего * объекта всегда подразумевало бы создание базового объекта, потому что base потребовалась бы для обработки деталей реализации, поэтому я подумал, что также целесообразно, чтобы base позаботилась о подсчете QueryInterface & Reference, но я запутался по следующим пунктам:
1) компилятор жалуется на неоднозначность членов из-за параллельной иерархии; IUnknown повторно реализован несколько раз из моего пользовательского интерфейса и из дополнительных интерфейсов IOle*. Документация предполагает, что ожидается, что на самом деле необходима только одна реализация для каждого объекта, но я не понимаю, как бы я решил проблемы компилятора, и я чувствую, что выполнение приведения типов как-то неправильно? Мне также интересно, должен ли я иметь все интерфейсы, унаследованные виртуально, что , по-видимому, справедливо для C++, хотя в COM такого понимания не было бы, но это не должно волновать (?).
2) Однако, если я объявляю все унаследованные интерфейсы как виртуальные в файлах.h, компилятор тогда жалуется, что унаследованные члены не допускаются, когда я пытаюсь реализовать QueryInterface в Base class.cpp. Я погуглил об этой ошибке, но не ясно, что она пытается сказать мне здесь.
РЕДАКТИРОВАТЬ: я ответил на свой вопрос. У Intel была документация по этой ошибке, которую я изначально не щелкал по ссылке, предполагая, что она может не относиться к Visual Studio. Хотелось бы, чтобы я все равно это сделал, но теперь я понимаю, почему я получаю эту ошибку, поскольку я пытался выполнить всю реализацию в Base::, а не IUnknown:: или IDispatch::. Теперь возникает новый вопрос, который может прояснить мой первоначальный и главный вопрос - как мне отложить реализацию от IUnknown (и других) до Base и работать только с Base, если это вообще возможно? Кажется, что если я просто использую IUnknown::xxx, он больше не сможет получить доступ к закрытым членам Базы, что, как мне кажется, вполне разумно, так что это может быть не тем, что я хочу. Я попытался объявить все другие интерфейсы, кроме собственного базового, как виртуальные, но на самом деле это не так. (Опять же, возможно, моя неопытность не видела очевидного решения.)
3) QueryInterface Base не может приводить base к потомку, что является разумной жалобой, поэтому я предполагаю, что мне все равно придется переопределить QI для всех потомков, но я могу делегировать обратно в QI базы, как только я определю, что запрошенные интерфейсы не являются дочерними. Как ни странно, компилятор настаивает на том, что дочерний * класс является абстрактным из-за отсутствия элементов для IUnknown & IDispatch, но разве база уже не реализована, и, следовательно, дочерний элемент должен также иметь эти члены?
Различные ошибки компилятора заставляют меня беспокоиться о том, что моя неопытность по отношению к одному или обоим языкам и фреймворкам побуждает меня делать ошибочные предположения о том, как я могу проектировать COM-объекты, иерархию наследования и детали реализации, и я определенно что-то здесь упускаю. Любые указатели, даже пощечина будет высоко ценится.
Спасибо!
1 ответ
Здесь у вас есть правильная идея, все, чего вам не хватает, - это связать некоторые слабые стороны в самом производном классе. Как разработчик COM, вы ожидаете, что все значения AddRef/Release/QI для объекта класса будут одинаковыми; но компилятор, ориентированный на C++, этого не знает, поэтому рассматривает их как потенциально отдельные. Здесь у вас есть два вывода: одно в базе и одно в любых интерфейсах, которые вы добавили.
Установить компилятор прямо здесь довольно просто: в самом производном классе переопределите все методы IUnknown и направьте их в соответствующий базовый класс - например.
class ChildX: public Base,
public IChildA
... more COM interfaces if needed ...
{
...
// Direct IUnknown methods to Base which does the refcounting for us...
STDMETHODIMP_(ULONG) AddRef() { return Base::AddRef(); }
STDMETHODIMP_(ULONG) Release() { return Base::Release(); }
... suggest implementing QI explicitly here.
}
Это в основном говорит о том, что все методы с именем AddRef, независимо от того, как они оказались в ChildX, получат эту конкретную реализацию.
Здесь проще всего реализовать QI напрямую, и только делегировать AddRef / Release в Base. (Технически, Base может приводиться к Child с использованием static_cast, но вам нужно поместить код в функцию после того, как Child был полностью определен; однако делать это не рекомендуется, так как у базового класса редко бывает хорошая причина знать о классы, которые вытекают из него.)
Другие вещи, на которые нужно обратить внимание: убедитесь, что в Base объявлен виртуальный dtor - даже если он просто пуст, так что когда Base выполняет "delete this", когда ref обращается к 0, он вызывает dtors в производных классах и любых ресурсах. что они выделили, убираются соответствующим образом. Кроме того, убедитесь, что подсчет ссылок правильный и поточнобезопасный при необходимости; сверьтесь с любым хорошим введением в книгу COM (например, "Внутри распределенного COM", который, несмотря на название, начинается с простого COM), чтобы увидеть, как другие люди делают это.
Это очень распространенная идиома в COM, и многие фреймворки используют либо макросы #define, либо более производный шаблонный класс для добавления в AddRef/Release/QI (как это делает MFC) в наиболее производном классе, а затем делегируют их хорошо известный базовый класс, который обрабатывает большую часть домашнего хозяйства.