Самоизменяющиеся записи виртуальной таблицы для указания на конкретную реализацию - PullRequest
2 голосов
/ 21 июля 2011

Короткая версия:

Может ли класс COM изменять свои записи виртуальной таблицы во время выполнения? (не учитывая темы)

Полная версия:

Я предоставляю несколько классов C ++, которые реализуют интерфейс . Интерфейс COM определен в стороннем фреймворке.

( Отредактировано: Vtable интерфейса COM является стандартным на платформе Windows, но мне не понятны взаимоотношения между C ++ vtable и COM vtable.)

Чтобы правильно внедрить мои реализации в эту платформу, необходимо использовать двухэтапную инициализацию: сначала создайте объект без параметров, затем вызовите метод Initialize со всеми параметрами. Это момент, когда можно выбрать одну из моих реализаций.

Чтобы это произошло, я добавил «resolver» класс (или просто класс-обертку), единственная ответственность которого заключается в выборе реализации в методе Initialize. После этого каждый вызов его COM-методов будет перенаправлен фактической реализации.

Класс резолвера затем внедряется в каркас. Это позволяет обойти ограничения рамок.

Теперь, видя, что класс резолвера после инициализации действительно бесполезен, мне интересно, есть ли способ избавиться от стоимости косвенного обращения к виртуальному методу? Моя идея - скопировать каждую запись COM vtable из конкретной реализации в vtable класса resolver.

Будет ли это работать?

Пример:

// (FYI) #define HRESULT unsigned long

struct IInterface
{
    // ... the usual AddRef, Release and QI omitted 

    virtual HRESULT Initialize(VARIANT v) = 0;  // the Initialize method, where implementation gets chosen
    virtual HRESULT DoWork() = 0;               // can only call after initialization.
};

class MyResolver : public IInterface
{
    // ... the usual AddRef, Release and QI omitted 
public:
    virtual HRESULT Initialize(VARIANT v) 
    {
        if ( /* some conditions based on v */ )
            return Implem_One.Create((void**) &m_pImpl);
        else
            return Implem_Two.Create((void**) &m_pImpl);

        "Here, can I copy the vtable entries from m_pImpl to the vtable of MyResolver (*this) ?";
        for (int k = 0; k < num_virtual_methods; ++k)
            this->vtbl[k] = m_pImpl->vtbl[k];
    }
    virtual HRESULT DoWork()
    {
        if (!m_pImpl) return ERROR_NOT_INITIALIZED;
        m_pImpl->DoWork();
    }
public:
    // this creation method is injected into the framework
    static HRESULT Create(void**) { /* create an instance of this class and return it */ }
private:
    MyResolver() : m_pImpl(NULL) {}
    virtual ~MyResolver() { if (m_pImpl) m_pImpl->Release(); }
    IInterface* m_pImpl;
};

1 Ответ

2 голосов
/ 21 июля 2011

Вы не можете безопасно копировать записи vtable; поскольку указатель this будет по-прежнему ссылаться на ваш прокси-класс, реальная реализация не найдет свои личные данные.

Если базовые классы поддерживают COM-агрегацию , вы можете использовать это, чтобы избежать накладных расходов. При создании базового объекта вы передаете IUnknown вашего внешнего класса как IUnknown внешнего агрегата. Это означает, что QueryInterface / AddRef / Release на всех производных интерфейсах базового объекта теперь будет делегироваться вашему внешнему объекту, делая его похожим на часть вашего внешнего объекта.

Теперь вы можете сделать так, чтобы QueryInterface возвращал ваш исходный объект (с методами проксирования) до инициализации или интерфейс базового объекта непосредственно после завершения инициализации. Пример:

class MyProxy : public IInterface
{
  long refCt;
  IUnknown *pUnkInner;
  IInterface *pIfaceInner;

  ~MyProxy() {
    AddRef(); // pUnkInner may take references to us in its destruction; prevent reentrancy per aggregating object rules
    if (pUnkInner) {
      pUnkInner->Release();
    }
  }
public:
  MyProxy() : refCt(1), pUnkInner(NULL), pIfaceInner(NULL) { }

  STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject)
  {
    if (!ppvObject) return E_POINTER;

    if (riid == IID_IUnknown) {
      AddRef();
      *ppvObject = (void *)static_cast<IUnknown *>(this);
      return S_OK;
    } else if (riid == IID_IInterface && pUnkInner) {
      // increments refcount of _outer_ object
      return pUnkInner->QueryInterface(riid, ppvObject);
    } else if (riid == IID_IInterface) {
      AddRef();
      *ppvObject = (void *)static_cast<IInterface *>(this);
      return S_OK;
    } else {
      return E_NOINTERFACE;
    }
  }

  STDMETHODIMP_(DWORD) AddRef(void)
  { return InterlockedIncrement(&refCt); }
  STDMETHODIMP_(DWORD) Release(void)
  { if (!InterlockedDecrement(&refCt)) delete this; }

  HRESULT Initialize(VARIANT v) {
    // You can use another protocol to create the object as well as long as it supports aggregation
    HRESULT res = CoCreateInstance(CLSID_InnerClass, this, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID *)&pUnkInner);
    if (FAILED(res)) return res;

    res = pUnkInner->QueryInterface(IID_IInterface, (void **)&pIfaceInner);
    if (FAILED(res)) {
      pUnkInner->Release();
      pUnkInner = NULL;
      return res;
    }

    Release(); // the above QueryInterface incremented the _outer_ refcount, we don't want that.

    return S_OK;
  }

  HRESULT DoWork() {
    if (!pIfaceInner) return ERROR_NOT_INITIALIZED;

    return pIfaceInner->DoWork();
  }
};

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

Тем не менее, жизненно важно, чтобы агрегированные объекты поддерживали протокол агрегации; в противном случае вы получите непоследовательные ссылки и другие ошибки. Внимательно прочитайте документацию MSDN перед выполнением агрегирования.

...