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

Сохраняемость в ATL

Глава из книги “ATL 8: внутренняя структура и применение”

Авторы: Кристофер Таваре
Кирк Фертитта
Брент Ректор
Крис Селлз

Источник: ATL 8: внутренняя структура и применение.
Материал предоставил: Издательский дом "Вильямс"
Опубликовано: 04.07.2007
Версия текста: 1.0
Обзор сохраняемости в COM
Интерфейсы IPropertyBag и IPersistPropertyBag
IPropertyBag2 и IPersistPropertyBag2
Реализация IPersistPropertyBag
Интерфейсы IStream, IPersistStreamInit и IPersistStream
Реализация IPersistStreamInit
IStorage и IPersistStorage
Классы реализации сохраняемости в ATL
Таблица свойств
Реализации сохраняемости
Таблица свойств
Реализация IPersistPropertyBagImpl
Реализация IPersistStreamInitImpl
Реализация IPersistStorageImpl
Дополнительные реализации сохраняемости
Интерфейс IPersistMemory
Реализация интерфейса IPersistMemory
Использование шаблона IPersistMemoryImpl
Добавление семантики маршалинга по значению с помощью сохраняемости
Резюме

Обзор сохраняемости в COM

Объекты, состояние которых нужно сохранять, должны реализовывать как минимум один (лучше несколько) интерфейс сохраняемости, который позволит контейнеру сохранить состояние объекта. Сохраняемым состоянием называют данные (обычно значения свойств и переменных состояния), которые нужно сохранить, прежде чем контейнер уничтожит объект. Контейнер передает вновь созданному объекту ранее сохраненное состояние, чтобы объект мог заново инициализироваться в состояние, в котором он пребывал до уничтожения.

COM не требует реализовывать в объектах поддержку сохраняемости и не использует такую поддержку, если она есть. COM просто описывает протокол, с помощью которого клиент может использовать поддержку сохраняемости, предоставляемую объектом. Эту модель сохраняемости часто называют управляемой клиентом, поскольку именно клиент в ней определяет, где нужно сохранить данные (на каком носителе) и когда выполнять сохранение.

COM определяет несколько интерфейсов, моделирующих носители данных, и предоставляет рабочие реализации для некоторых из этих интерфейсов. В именах таких интерфейсов обычно используется фрагмент IMedium, где вместо Medium может использоваться Stream, Storage, PropertyBag и т.д. Интерфейс носителя содержит, например, методы Read и Write, которые объект использует для загрузки и сохранения своего состояния.

Кроме того, COM описывает интерфейсы, которые объект реализует, если ему нужно поддерживать сохраняемость на различных носителях. В именах таких интерфейсов обычно используется фрагмент IPersistMedium. Интерфейсы сохраняемости обычно предоставляют методы вроде Load и Save, которые клиент вызывает, чтобы приказать объекту восстановить или сохранить его состояние. Клиент предоставляет объекту соответствующие интерфейсы носителей как аргументы при вызовах Load и Save. Эта модель показана на рис. 7.1.


Рис. 7.1. Управляемая клиентом модель сохраняемости

Все интерфейсы IPersistMedium являются производными от IPersist, который выглядит следующим образом.

interface IPersist : IUnknown
{ HRESULT GetClassID([out] CLSID* pclsid); }

Клиент использует метод GetClassID, когда ему требуется сохранить состояние объекта. Обычно клиент запрашивает интерфейс IPersistMedium, вызывает метод GetClassID, чтобы получить CLSID объекта, который хочет сохранить, и записывает CLSID на носитель. Затем клиент приказывает объекту сохранить его состояние на носитель. Восстановление состояния объекта выполняется в обратной последовательности: клиент считывает CLSID с носителя, создает экземпляр класса, запрашивает у созданного экземпляра интерфейс IPersistMedium и приказывает объекту загрузить его состояние с носителя.

Клиент может приказать объекту сохранить свое состояние двумя основными способами: в виде самоописывающего набора именованных свойств или в виде непрозрачного набора двоичных данных.

Если объект сохраняет свое состояние в виде самоописывающего набора именованных свойств, он передает клиенту набор пар “имя-значение”. Затем клиент сохраняет эти пары в формате, наиболее удобном с точки зрения клиента — например, в текстовом виде или в формате HTML. Преимущество такого способа представления данных, например, в виде тегов <param> и XML, заключается в возможности записи данных в одной программе и свободного чтения их в другой.

Представление своего состояния в виде потока байтов более эффективно с точки зрения объекта, поскольку ему не приходится предоставлять имена своих свойств и преобразовывать значения этих свойств в пары “имя-значение”. Кроме того, клиенту не приходится выполнять преобразование значений в текст и обратно. Однако байтовые потоки зависят от аппаратных платформ, — например, в расположении байтов и форматах представления символов или чисел с плавающей запятой — и объект, который в дальнейшем попытается прочитать данные из байтового потока, должен учитывать эту зависимость.

ATL поддерживает обе формы сохраняемости. Прежде чем мы рассмотрим реализацию сохраняемости в ATL, давайте посмотрим, как можно реализовать сохраняемость в COM напрямую.

Интерфейсы IPropertyBag и IPersistPropertyBag

Контейнеры элементов управления ActiveX, реализующие механизм “сохранения в виде текста”, обычно используют интерфейсы IPropertyBag и IPersistPropertyBag. Контейнер реализует IPropertyBag, а элемент управления — IPersistPropertyBag, чтобы показать, что он может сохранять свое состояние как самоописывающий набор именованных свойств.

interface IPropertyBag : public IUnknown {
  HRESULT Read([in] LPCOLESTR pszPropName,
    [in, out] VARIANT* pVar, [in] IErrorLog* pErrorLog);

  HRESULT Write([in] LPCOLESTR pszPropName, [in] VARIANT* pVar);
};

interface IPersistPropertyBag : public IPersist {
  HRESULT InitNew ();
  HRESULT Load([in] IPropertyBag* pPropBag,
    [in] IErrorLog* pErrorLog);
  HRESULT Save([in] IPropertyBag* pPropBag,
    [in] BOOL fClearDirty,
    [in] BOOL fSaveAllProperties);
};

Когда клиент (контейнер) хочет подробно указать, как нужно сохранять отдельные свойства объекта, он пытается использовать как механизм сохраняемости интерфейс IPersistPropertyBag объекта. Клиент предоставляет пакет свойств (property bag) объекту в виде интерфейса IPropertyBag.

Когда объект хочет считать свойство с помощью метода IPersistPropertyBag::Load, он вызывает метод IPropertyBag::Read. Когда объект сохраняет свойства с помощью IPersistPropertyBag::Save, он вызывает метод IPropertyBag::Write. Каждое свойство описывается именем в pszPropName, значение которого записывается в VARIANT. При операциях считывания пакет свойств предоставляет именованное свойство из пакета в форме, заданной входным значением VARIANT, если только тип — не VT_EMPTY; в этом случае пакет свойств предоставляет свойство в любом формате, удобном для пакета.

Информация, предоставляемая объектом о каждом свойстве (имя-тип-значение) во время операции сохранения позволяет клиенту, например, сохранять значения как текст. Это основная причина, по которой может использоваться IPersistPropertyBag. Ошибки, возникающие при чтении, записываются в специальный журнал.

IPropertyBag2 и IPersistPropertyBag2

Интерфейс IPropertyBag предоставляет объекту немного информации о свойствах, содержащихся в пакете. Поэтому появился новый интерфейс IPropertyBag2, дающий объекту гораздо больше возможностей доступа к информации о свойствах в пакете. Объекты, поддерживающие сохраняемость с помощью IPropertyBag2, реализуют интерфейс IPersistPropertyBag2.

interface IPropertyBag2 : public IUnknown {
  HRESULT Read([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
    [in] IErrorLog* pErrLog, [out] VARIANT* pvarValue,
    [out] HRESULT* phrError);
  HRESULT Write([in] ULONG cProperties, [in] PROPBAG2* pPropBag,
    [in] VARIANT* pvarValue);
  HRESULT CountProperties ([out] ULONG* pcProperties);
  HRESULT GetPropertyInfo([in] ULONG iProperty,
    [in] ULONG cProperties,
    [out] PROPBAG2* pPropBag, [out] ULONG* pcProperties);
  HRESULT LoadObject([in] LPCOLESTR pstrName, [in] DWORD dwHint,
    [in] IUnknown* pUnkObject, [in] IErrorLog* pErrLog);
};

interface IPersistPropertyBag2 : public IPersist {
  HRESULT InitNew ();
  HRESULT Load ([in] IPropertyBag2* pPropBag,
    [in] IErrorLog* pErrLog);
  HRESULT Save ([in] IPropertyBag2* pPropBag,
    [in] BOOL fClearDirty,
    [in] BOOL fSaveAllProperties);
  HRESULT IsDirty();
};

Интерфейс IPropertyBag2 представляет собой расширение IPropertyBag. Он позволяет объекту получать свойства из пакета и информацию о типе каждого свойства через методы CountProperties и GetPropertyInfo. Пакет свойств, реализующий IPropertyBag2, должен поддерживать и IPropertyBag, чтобы объекты, поддерживающие только IPropertyBag, могли обращаться к своим свойствам. Точно так же объект, поддерживающий IPersistPropertyBag2, должен поддерживать IPersistPropertyBag, чтобы взаимодействовать с пакетами свойств, поддерживающими только IPersistPropertyBag.

Когда объект хочет прочитать свойство в IPersistPropertyBag2::Load, он вызывает метод IPropertyBag2::Read. Когда объект сохраняет свойства в IPersistPropertyBag2::Save, он вызывает IPropertyBag2::Write. Клиент записывает ошибки, возникшие при чтении свойств, с помощью предоставляемого интерфейса IErrorLog.

Реализация IPersistPropertyBag

Клиент приказывает объекту инициализироваться только один раз. Если у клиента нет начальных значений, которые он хочет передать объекту, клиент вызывает метод объекта IPersistPropertyBag::InitNew. В этом случае объект должен инициализировать себя значениями по умолчанию. Если клиент передает объекту начальные значения, он загружает свойства в пакет свойств и вызывает метод объекта IPersistPropertyBag::Load. Когда клиент хочет сохранить состояние объекта, он создает пакет свойств и вызывает метод объекта IPersistPropertyBag::Save.

Реализация объекта — дело несложное. Например, у объектов класса Demagogue есть три свойства: имя, речь и громкость. Вот пример реализации, позволяющей сохранять и восстанавливать эти три свойства из пакета свойств.

class ATL_NO_VTABLE CDemagogue
    : public IPersistPropertyBag, ... {
    BEGIN_COM_MAP(CDemagogue)
      ...
      COM_INTERFACE_ENTRY(IPersistPropertyBag)
      COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
    END_COM_MAP()
    ...
    CComBSTR m_name;
    long     m_volume;
    CComBSTR m_speech;

    STDMETHODIMP Load(IPropertyBag *pBag, IErrorLog *pLog) {
      // Иницализируем VARIANT типом VT_BSTR
      CComVariant v ((BSTR) NULL);
      HRESULT hr = pBag->Read(OLESTR("Name"), &v, pLog);
      if (FAILED(hr)) return hr;
      m_name = v.bstrVal;

      // Инициализируем VARIANT типом VT_I4
      v = 0L;
      hr = pBag->Read(OLESTR("Volume"), &v, pLog);
      if (FAILED(hr)) return hr;
      m_volume = v.lVal;

      // Инициализируем VARIANT типом VT_BSTR
      v = (BSTR) NULL;
      hr = pBag->Read(OLESTR("Speech"), &v, pLog);
      if (FAILED (hr)) return hr;
      m_speech = v.bstrVal;

      return S_OK;
    }

    STDMETHODIMP Save(IPropertyBag *pBag,
      BOOL fClearDirty, BOOL /* fSaveAllProperties */) {
      CComVariant v = m_name;
      HRESULT hr = pBag->Write(OLESTR("Name"), &v);
      if (FAILED(hr)) return hr;

      v = m_volume;
      hr = pBag->Write(OLESTR("Volume"), &v);
      if (FAILED(hr)) return hr;

      v = m_speech;
      hr = pBag->Write(OLESTR("Speech"), &v);
      if (FAILED(hr)) return hr;

      if (fClearDirty) m_fDirty = FALSE;
      return hr;
    }
};

Интерфейсы IStream, IPersistStreamInit и IPersistStream

Объекты COM, которые хотят эффективно сохранять свое состояние в виде двоичных потоков байтов, обычно реализуют интерфейсы IPersistStream и IPersistStreamInit. Элементы управления ActiveX с сохраняемыми состояниями должны как минимум реализовывать IPersistStream или IPersistStreamInit. Эти два интерфейса являются взаимоисключающими, и обычно их не реализуют одновременно. Элемент управления реализует IPersistStreamInit, когда он хочет знать, был ли он создан с нуля или создан и инициализирован из ранее сохраненного состояния. Интерфейс IPersistStream не позволяет сообщить элементу, что он создан с нуля. Существование какого-либо из этих интерфейсов показывает, что элемент управления может сохранять свое состояние в потоке и загружать состояние из потока — т.е. из реализации IStream.

Интерфейс IStream тесно связан с файловым API Win32, и его легко реализовывать с носителями, работающими с байтами. COM предоставляет две реализации IStream — отображающуюся в файл OLE Structured Storage и отображающуюся в буфер в памяти.

interface ISequentialStream : IUnknown {
    HRESULT Read([out] void *pv, [in] ULONG cb,
        [out] ULONG *pcbRead);

    HRESULT Write( [in] void const *pv, [in] ULONG cb,
        [out] ULONG *pcbWritten);
}

interface IStream : ISequentialStream {
  HRESULT Seek([in] LARGE_INTEGER dlibMove,
               [in] DWORD dwOrigin,
               [out] ULARGE_INTEGER *plibNewPosition);
  //...
}

interface IPersistStreamInit : public IPersist {
        HRESULT IsDirty();
        HRESULT Load([in] LPSTREAM pStm);
        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
        HRESULT InitNew();
};

interface IPersistStream : public IPersist {
        HRESULT IsDirty();
        HRESULT Load([in] LPSTREAM pStm);
        HRESULT Save([in] LPSTREAM pStm, [in] BOOL fClearDirty);
        HRESULT GetSizeMax([out] ULARGE_INTEGER*pCbSize);
};

Когда клиент хочет, чтобы объект сохранил свое состояние в виде непрозрачного потока байтов, он обычно пытается использовать интерфейс объекта IPersistStreamInit как механизм сохраняемости. Клиент предоставляет поток, в который объект выполняет сохранение, в виде интерфейса IStream.

Когда клиент вызывает метод IPersistStreamInit::Load, объект считывает значения своих свойств из потока, вызывая метод IStream::Read. Когда клиент вызывает метод IPersistStreamInit::Save, объект записывает в поток значения своих свойств, вызывая метод IStream::Write. Заметьте, что если объект не предпримет дополнительных действий, поток будет содержать значения в специфичном для аппаратной архитектуры порядке байтов.

Новые клиенты предпочитают использовать интерфейс объекта IPersistStreamInit; если такого интерфейса нет, они пытаются обратиться к IPersistStream. Однако более старые клиенты могут использовать только IPersistStream. Чтобы быть совместимым со старыми клиентами, ваш объект должен поддерживать IPersistStream. Однако некоторые клиенты могут работать только с IPersistStreamInit, и чтобы быть совместимым с новыми клиентами, ваш объект должен поддерживать IPersistStreamInit.

Однако обычно от объекта не ждут реализации обоих интерфейсов, поскольку в таком случае клиенту будет неясно, нужно ли сообщать объекту, что он создан с нуля. Лучшее решение этой дилеммы — поддерживать оба интерфейса, если объекту не нужно сообщать, что он создан с нуля, даже если это нарушает спецификацию элемента управления.

Хотя IPersistStreamInit и не является производным от IPersistStream (он не может быть его производным из-за взаимоисключающего характера этих интерфейсов), они обладают идентичными таблицами vtbl для всех методов, кроме последнего — метода InitNew. Учитывая бинарную совместимость COM, если объекту не нужно вызывать метод InitNew, он может выдавать указатель на интерфейс IPersistStreamInit, когда у него запрашивают интерфейс IPersistStream. Так что с помощью реализации IPersistStreamInit и одной дополнительной записи в таблице интерфейсов можно сделать объект совместимым со всеми клиентами.

class ATL_NO_VTABLE CDemagogue : public IPersistStreamInit, ... {
...
BEGIN_COM_MAP(CDemagogue)
  ...
  COM_INTERFACE_ENTRY(IPersistStreamInit)
  COM_INTERFACE_ENTRY_IID(IID_IPersistStream, IPersistStreamInit)
  COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
END_COM_MAP()
...
};

Реализация IPersistStreamInit

Клиент приказывает объекту инициализироваться только один раз. Если у клиента нет начальных значений для передачи объекту, клиент вызывает метод объекта IPersistStreamInit::InitNew. В этом случае объект должен инициализировать себя значениями по умолчанию. Если у клиента есть начальные значения, которые он хочет передать объекту, клиент открывает поток и вызывает метод объекта IPersistStreamInit::Load. Когда клиент хочет сохранить состояние объекта, он создает поток и вызывает метод объекта IPersistStreamInit::Save.

Как и при использовании пакета свойств, реализация объекта будет довольно прямолинейной. Вот пример реализации для объектов Demagogue, позволяющей сохранять в потоке значения трех свойств и восстанавливать эти значения из потока.

class CDemagogue : public IPersistStreamInit, ... {
    CComBSTR m_name;
    long     m_volume;
    CComBSTR m_speech;
    BOOL     m_fDirty;

    STDMETHODIMP IsDirty() { return m_fDirty ? s_OK : S_FALSE; }

    STDMETHODIMP Load(IStream* pStream) {
        HRESULT hr = m_name.ReadFromStream(pStream);
        if (FAILED (hr)) return hr;

        ULONG cb;
        hr = pStream->Read (&m_volume, sizeof (m_volume), &cb);
        if (FAILED (hr)) return hr;
        hr = m_speech.ReadFromStream(pStream);
        if (FAILED (hr)) return hr;
        m_fDirty = FALSE ;
        return S_OK;
    }

    STDMETHODIMP Save (IStream* pStream) {
        HRESULT hr = m_name.WriteToStream (pStream);
        if (FAILED(hr)) return hr;

        ULONG cb;
        hr = pStream->Write(&m_volume, sizeof (m_volume), &cb);
        if (FAILED(hr)) return hr;

        hr = m_speech.WriteToStream (pStream);
        return hr;
    }

    STDMETHODIMP GetSizeMax(ULARGE_INTEGER* pcbSize) {
        if (NULL == pcbSize) return E_POINTER;
        // Длина m_name
        pcbSize->QuadPart = CComBSTR::GetStreamSize(m_name);
        pcbSize->QuadPart += sizeof (m_volume);
        // Длина of m_speech
        pcbSize->QuadPart += CComBSTR::GetStreamSize(m_speech);
        return S_OK;
    }

    STDMETHODIMP InitNew() { return S_OK; }
};

IStorage и IPersistStorage

Встраиваемый объект — т.е. объект, который можно сохранять в контейнере OLE, например, в Microsoft Word или Microsoft Excel, должен реализовывать интерфейс IPersistStorage. Контейнер предоставляет объекту указатель на интерфейс IStorage. Этот указатель указывает на структурированное хранилище. Объект-хранилище, реализующий интерфейс IStorage, работает примерно как папка в обычной файловой системе. Объект может использовать интерфейс IStorage для создания новых и открытия существующих подхранилищ и потоков в хранилище, которое предоставляет контейнер.

interface IStorage : public IUnknown {
  HRESULT CreateStream([string,in] const OLECHAR* pwcsName,
    [in] DWORD grfMode, [in] DWORD reserved1,
    [in] DWORD reserved2,
    [out] IStream** ppstm);

  HRESULT OpenStream([string,in] const OLECHAR* pwcsName,
    [unique][in] void* reserved1, [in] DWORD grfMode,
    [in] DWORD reserved2, [out] IStream** ppstm);

  HRESULT CreateStorage([string,in] const OLECHAR* pwcsName,
    [in] DWORD grfMode, [in] DWORD reserved1,
    [in] DWORD reserved2,
    [out] IStorage** ppstg);

  HRESULT OpenStorage(
    [string,unique,in] const OLECHAR* pwcsName,
    [unique,in] IStorage* pstgPriority,
    [in] DWORD grfMode, [unique,in] SNB snbExclude,
    [in] DWORD reserved, [out] IStorage** ppstg);

  // Остальные методы только упомянем для краткости...
  HRESULT CopyTo( ... );
  HRESULT MoveElementTo( ... )
  HRESULT Commit( ... )
  HRESULT Revert(void);
  HRESULT EnumElements( ... );
  HRESULT DestroyElement( . . , );
  HRESULT RenameElement( ... );
  HRESULT SetElementTimes( ... );
  HRESULT SetClass( ... );
  HRESULT SetStateBits( ... );
  HRESULT Stat( ... );
};

interface IPersistStorage : public IPersist {
  HRESULT IsDirty ();
  HRESULT InitNew ([unique,in] IStorage* pStg);
  HRESULT Load ([unique,in] IStorage* pStg);
  HRESULT Save ([unique,in] IStorage* pStgSave,
    [in] BOOL fSameAsLoad);
  HRESULT SaveCompleted ([unique,in] IStorage* pStgNew);
  HRESULT HandsOffStorage ();
};

Методы IsDirty, InitNew, Load и Save работают так же, как и в ранее рассмотренных нами интерфейсах сохраняемости. Однако в отличие от потоков, когда контейнер передает объекту указатель на интерфейс IStorage при вызовах методов InitNew и Load, объект может сохранять этот указатель (разумеется, вызвав для него метод AddRef). Это позволит объекту выполнять инкрементное считывание и запись своего состояния вместо того, чтобы считывать его сразу целиком, как в других механизмах сохраняемости. Чтобы приказать объекту освободить сохраненный указатель, клиент использует метод HandsOffStorage, а чтобы указать использовать новый интерфейс IStorage метод SaveCompleted.

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

Для многих объектов это слишком накладная схема обеспечения сохраняемости. Простые объекты, например Demagogue, который мы рассматривали, вряд ли нуждаются в гибкости, которая предоставляется этой схемой. Часто такие объекты просто создают поток в выделенном им хранилище и сохраняют свое состояние в этом потоке с помощью интерфейса IPersistStreamInit. Именно это и делает реализация IPersistStorage в ATL, поэтому в этом разделе мы не будем создавать собственную реализацию IPersistStorage — все равно мы скоро рассмотрим реализацию из ATL.

Классы реализации сохраняемости в ATL

ATL предоставляет реализации интерфейсов IPersistPropertyBag, IPersistStreamInit и IPersistStorage, которые называются, соответственно, IPersistPropertyBagImpl, IPersistStreamInitImpl и IPersistStorageImpl. Каждый класс-шаблон принимает один параметр — имя производного класса. Добавить поддержку этих интерфейсов в своем классе можно следующим образом.

class ATL_NO_VTABLE CDemagogue :
    public IPersistPropertyBagImpl<CDemagogue>,
    public IPersistStreamInitImpl<CDemagogue>,
    public IPersistStorageImpl<CDemagogue> {
...
BEGIN_COM_MAP(CDemagogue)
    ...
    COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    COM_INTERFACE_ENTRY(IPersistStream)
    COM_INTERFACE_ENTRY(IPersistStorage)
END_COM_MAP()
    ...
};

Не забудьте добавить в таблицу интерфейсов запись для IPersist. Все три интерфейса сохраняемости являются производными от IPersist, а не IUnknown, поэтому ваш класс должен отвечать утвердительно на запросы IPersist. Кроме того, заметьте, что вам нужно будет использовать макрос COM_INTERFACE_ENTRY2 (или COM_INTERFACE_ENTRY_IID), поскольку от IPersist порождены несколько базовых классов вашего класса.

Таблица свойств

Реализация этих трех интерфейсов сохраняемости в ATL требует, чтобы ваш объект предоставлял таблицу свойств (property map). В этой таблице должны быть описаны все свойства, которые необходимо загружать и сохранять с помощью интерфейсов сохраняемости. ATL использует таблицу свойств класса для двух разных задач: поддержки сохраняемости и поддержки страниц свойств элементов управления (эти страницы обсуждаются в главе 11, “Элементы управления ActiveX”).

Различные записи в таблице свойств позволяют следующее.

Таблица свойств класса CDemagogue выглядит так.

BEGIN_PROP_MAP(CDemagogue)
  PROP_ENTRY_EX("Speech", DISPID_SPEECH,
    CLSID_NULL, IID_ISpeaker)
  PROP_ENTRY_EX("Volume", DISPID_VOLUME,
    CLSID_NULL, IID_ISpeaker)
  PROP_ENTRY_EX("Name", DISPID_NAME,
    CLSID_NULL, IID_INamedObject)
END_PROP_MAP()

Макросы BEGIN_PROP_MAP и END_PROP_MAP определяют таблицу свойств класса. Свойства объектов, для которых должна поддерживаться сохраняемость, перечисляются в таблице свойств с помощью макросов PROP_ENTRY и PROP_ENTRY_EX. Макрос PROP_ENTRY описывает свойство, к которому реализация сохраняемости может обращаться через интерфейс диспетчера по умолчанию (т.е. через интерфейс, получаемый при запросе IID_IDispatch). Макрос PROP_ENTRY_EX описывает свойство, к которому реализация сохраняемости должна обращаться через какой-то другой специализированный интерфейс диспетчера. Оба макроса используют имя свойства, DISPID свойства, и значение связанной с этим свойством страницы свойств CLSID (страницы свойств рассматриваются в главе 11). Кроме того, макрос PROP_ENTRY_EX также требует IID интерфейса диспетчера, поддерживающего соответствующее свойство; макрос PROP_ENTRY использует IID_IDispatch.

PROP_ENTRY (szDesc, dispid, clsid)
PROP_ENTRY_EX (szDesc, dispid, clsid, iidDispatch)
PROP_DATA_ENTRY (szDesc, member, vt)

Кроме того, можно загружать и сохранять значения переменных класса, недоступных через интерфейсы диспетчеров. Макрос PROP_DATA_ENTRY позволяет указать имя свойства, переменную, содержащую значение этого свойства, и тип VARIANT этой переменной, например:

BEGIN_PROP_MAP(CBullsEye)
  PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
  PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
  ...
END_PROP_MAP()

Практически PROP_DATA_ENTRY заставляет реализацию сохраняемости просмотреть объект, извлечь из указанной переменной значение с длиной, заданной указанным типом, поместить полученные данные в переменную типа VARIANT и записать эту переменную на носитель. Это удобно, если вам нужно сохранить значение переменной, совместимой по типу с VARIANT. Однако этот механизм не работает, если требуется сохранить переменную несовместимого с VARIANT типа, например индексированного свойства. Макрос PROP_PAGE используется для связывания свойств со страницами свойств; его мы обсудим в главе 11, “Элементы управления ActiveX”. Реализации сохраняемости пропускают записи в таблицах свойств, сделанные макросами PROP_PAGE.

Одно предупреждение: не добавляйте в таблицу с помощью макросов PROP_ENTRY, PROP_ENTRY_EX или PROP_DATA_ENTRY свойства с пробелами в именах. Некоторые популярные контейнеры, например, Visual Basic 6, используют реализации IPropertyBag, не способные обрабатывать имена с пробелами.

Если у вас есть переменная-член класса, значение которой вы хотите сохранять и загружать с помощью механизмов сохраняемости, а ее тип не совместим с VARIANT (например, является массивом или перечислением), то механизм таблиц свойств вам не поможет. Вам нужно будет переопределить соответствующие методы в реализациях интерфейсов сохраняемости и явно считывать и записывать значение переменной. Чтобы сделать это, вы должны знать базовую структуру реализации сохраняемости в ATL.

Реализации сохраняемости

Давайте рассмотрим реализации сохраняемости на примере реализации, использующей пакеты свойств. Все реализации сохраняемости устроены и работают примерно одинаково.

Таблица свойств

Макросы таблицы свойств практически добавляют в класс статические методы GetProperty. Метод GetPropertyMap возвращает указатель на массив структур ATL_PROPMAP_ENTRY. Эти структуры выглядят так.

struct ATL_PROPMAP_ENTRY {
    LPCOLESTR szDesc;
    DISPID dispid;
    const CLSID* pclsidPropPage;
    const IID* piidDispatch;
    DWORD dwOffsetData;
    DWORD dwSizeData;
    VARTYPE vt;
};

Например, вот таблица свойств и получаемый при обработке макросов код.

BEGIN_PROP_MAP(CDemagogue)
    PROP_ENTRY("Speech", DISPID_SPEECH, CLSID_NULL)
    PROP_ENTRY_EX("Name", DISPID_NAME, CLSID_NULL,
      IID_INamedObject)
    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
END_PROP_MAP()

Эта таблица свойств преобразуется в такой код.

__if_not_exists(__ATL_PROP_NOTIFY_EVENT_CLASS) {
    typedef ATL::_ATL_PROP_NOTIFY_EVENT_CLASS
        __ATL_PROP_NOTIFY_EVENT_CLASS;
}
static ATL::ATL_PROPMAP_ENTRY* GetPropertyMap() {
  static ATL::ATL_PROPMAP_ENTRY pPropMap[] = {
  {OLESTR(("Speech"), DISPID_SPEECH, &CLSID_NULL,
    __uudiof(IID_IDispatch), 0, 0, 0},
  {OLESTR("Name"), DISPID_ NAME, &CLSID_NULL,
    &IID_INamedObject, 0, 0, 0},
  {OLESTR("_cx"), 0, &CLSID_NULL, NULL,
    offsetof(_PropMapClass, m_sizeExtent.cx),
    sizeof(((_PropMapClass*)0)-> m_sizeExtent.cx), VT_UI4},
  {NULL, 0, NULL, &IID_NULL, 0, 0, 0}
  };
  return pPropMap;
}

В поле szDesc структуры хранится имя свойства. Оно используется только в реализации сохраняемости с помощью пакета свойств.

В поле dispid хранится идентификатор диспетчера свойства. Все реализации сохраняемости используют этот идентификатор для доступа к свойству через одну из реализаций IDispatch в объекте — с помощью метода Invoke.

В поле pclsidPropPage хранится указатель на CLSID для страницы свойств, связанной с объектом. Он не используется во время работы механизмов сохраняемости.

В поле piidDispatch хранится указатель на IID интерфейса диспетчера, поддерживающего это свойство. Указанный dispid уникален для данного интерфейса.

Последние три поля используются только макросами PROP_DATA_ENTRY. В поле dwOffsetData хранится величина смещения переменной, к которой относится данная запись в таблице свойств, от начала экземпляра класса. В поле dwSizeData хранится размер этой переменной в байтах, а в поле vt — тип VARIANT переменной (код перечисления VARTYPE).

Различные реализации сохраняемости практически просто перебирают записи в таблице свойств и сохраняют или загружают поочередно все свойства. Если свойства включены в таблицу с помощью макросов PROP_ENTRY и PROP_ENTRY_EX, реализации вызывают метод IDispatch::Invoke с соответствующим dispid для считывания или записи этих свойств.

Invoke передает каждое свойство через тип VARIANT. Потоковая реализация сохраняемости просто помещает значение типа VARIANT в объект класса CComVariant и использует методы WriteToStream и ReadFromStream этого объекта для выполнения чтения и записи. Соответственно, потоковая реализация сохраняемости поддерживает все типы VARIANT, поддерживаемые классом CComVariant (мы рассматривали этот класс в главе 3, “Интеллектуальные типы ATL”). Реализация сохраняемости с помощью пакетов свойств еще проще, поскольку пакеты свойств прямо работают с типом VARIANT.

Если свойства включены в таблицу свойств с помощью макросов PROP_DATA_ENTRY, жизнь становится сложнее. Реализация IPersistStreamInit непосредственно обращается к элементу объекта с указанными смещением и длиной и считывает значения требуемых байтов.

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

Реализация IPersistPropertyBagImpl

Методы интерфейса IPersistPropertyBag реализует класс IPersistPropertyBagImpl<T>. Как и все интерфейсы сохраняемости, IPersistPropertyBag наследует IPersist, в котором содержится единственный метод: GetClassID.

interface IPersist : IUnknown
{ HRESULT GetClassID([out] CLSID* pclsid); }

Все классы реализации сохраняемости используют одинаковые реализации GetClassID. Они вызывают статический метод GetObjectCLSID для получения CLSID. Этот метод должен предоставляться производным классом (вашим классом) или одним из его базовых классов.

template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
    : public IPersistPropertyBag {
public:
    ...
    STDMETHOD(GetClassID)(CLSID *pClassID) {
        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::GetClassID\n"));
        if (pClassID == NULL)
            return E_POINTER;
        *pClassID = T::GetObjectCLSID();
        return S_OK;
    }};

Обычно ваш класс получает статический метод GetObjectCLSID от CComCoClass.

template <class T, const CLSID* pclsid = &CLSID_NULL>
class CComCoClass {
public:
    ...
    static const CLSID& WINAPI GetObjectCLSID() {return *pclsid;}
};

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

Класс IPersistPropertyBagImpl<T> также реализует остальные методы IPersistPropertyBag, включая, например, метод Load. Метод IPersistPropertyBagImpl<T>::Load вызывает T::IPersistPropertyBag_Load для выполнения большей части работы. Это позволяет вашему классу предоставить этот метод, если вам нужна специальная версия Load. Обычно ваш класс (T) не предоставляет метода IPersistPropertyBag_Load, поэтому его вызов сводится к вызову реализации по умолчанию, предоставляемой базовым классом — IPersistPropertyBagImpl<T>::IPersistPropertyBag_Load. Реализация по умолчанию вызывает глобальную функцию AtlIPersistPropertyBag_Load. Эта глобальная функция перебирает записи в таблице свойств и загружает соответствующее каждой записи свойство из пакета свойств.

template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
    : public IPersistPropertyBag {
public:
    ...
    // IPersistPropertyBag
    //
    STDMETHOD(Load)(LPPROPERTYBAG pPropBag,
        LPERRORLOG pErrorLog) {
        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Load\n"));
        T* pT = static_cast<T*>(this);
        ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
        ATLASSERT(pMap != NULL);
        return pT->IPersistPropertyBag_Load(pPropBag,
            pErrorLog, pMap);
    }
    HRESULT IPersistPropertyBag_Load(LPPROPERTYBAG pPropBag,
        LPERRORLOG pErrorLog, ATL_PROPMAP_ENTRY* pMap) {
        T* pT = static_cast<T*>(this);
        HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag,
            pErrorLog, pMap, pT, pT->GetUnknown());
        if (SUCCEEDED(hr))
            pT->m_bRequiresSave = FALSE;
        return hr;
    }
    ...
};

Такая структура реализации предоставляет три места, в которых можно переопределить методы и предоставить специализированую поддержку сохраняемости для свойств, не совместимых с типом VARIANT. Можно переопределить сам метод Load, практически напрямую реализуя метод IPersistPropertyBag. Можно также позволить ATL реализовать метод Load и реализовать в проекте метод IPersistPropertyBag_Load. И наконец, мы можем позволить ATL реализовать Load и IPersistPropertyBag_Load, а сами создадим собственный вариант глобальной функции ATLIPersistPropertyBag_Load и с помощью нескольких фокусов с компоновщиком заставим наш объект использовать эту глобальную функцию, а не функцию, предоставляемую ATL.

Проще всего реализовать метод Load. Обычно при его реализации вызывается метод Load базового класса, считывающий все свойства, перечисленные в таблице свойств, а затем считываются свойства, не совместимые с VARIANT типами:

HRESULT CMyObject::Load(LPPROPERTYBAG pPropBag,
    LPERRORLOG pErrorLog) {
    HRESULT hr =
IPersistPropertyBagImpl<CMyObject>::Load(pPropBag,pErrorLog);
    if (FAILED (hr)) return hr;

    // Считываем массив значений VT_I4
    // Нам придется создать "имя" для каждого элемента массива
    // Считываем каждый элемент как VARIANT, а затем воссоздаем массив
...
}

У такого подхода есть несколько недостатков. Это незначительный момент, но теперь объект будет требовать четырех методов для реализации сохраняемости: метода Load своего класса, метода Load базового класса, метода IPersistPropertyBag_Load базового класса и AtlIPersistPropertyBag_Load. Мы могли бы скопировать реализацию Load из базового класса в метод Load своего класса, но это снизит надежность нашего класса, поскольку в будущих версиях ATL методы реализации сохраняемости могут измениться.

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

STDMETHOD(Load)(LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog) {
    ...
    T* pT = static_cast<T*>(this);
    ...
    return pT->IPersistPropertyBag_Load(pPropBag, pErrorLog, pMap);
}

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

Вообще говоря, лучшее решение — позволить объекту предоставить собственную реализацию метода IPersistPropertyBag_Load. В этой реализации объект может вызвать глобальную функцию ATLIPersistPropertyBag_Load и сохранить любые свои свойства, не совместимые с VARIANT. Вот пример из элемента управления BullsEye, который мы рассмотрим в главе 11, “Элементы управления ActiveX”. Он содержит свойство, представляющее собой массив целых чисел типа long. Этот массив нельзя описать совместимым с VARIANT типом, поскольку это не SAFEARRAY, и соответственно мы не можем описать его в таблице свойств.

HRESULT CBullsEye::IPersistPropertyBag_Load(
    LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog,
    ATL_PROPMAP_ENTRY* pMap) {
    if (NULL == pPropBag) return E_POINTER;

    // Загружаем свойства, описанные в PROP_MAP
    HRESULT hr = AtlIPersistPropertyBag_Load(pPropBag, pErrorLog,
        pMap, this, GetUnknown());
    if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;

    if (FAILED (hr)) return hr;

    // Загружаем индексированное свойство - RingValues
    // Получаем количество колец
    short sRingCount;
    get_RingCount (&sRingCount);

    // Считываем значение для каждого кольца
    for (short nIndex = 1; nIndex <= sRingCount; nIndex++) {

        // Создаем базовое имя свойства
        CComBSTR bstrName = OLESTR("RingValue");

        // Создаем номер кольца как строку
        CComVariant vRingNumber = nIndex;
        hr = vRingNumber.ChangeType (VT_BSTR);
        ATLASSERT (SUCCEEDED (hr));

        // Конкатенируем две строки, чтобы получить имя свойства
        bstrName += vRingNumber.bstrVal;

        // Считываем значение для кольца из пакета свойств
        CComVariant vValue = 0L;
        hr = pPropBag->Read(bstrName, &vValue, pErrorLog);
        ATLASSERT (SUCCEEDED (hr));
        ATLASSERT (VT_I4 == vValue.vt);

        if (FAILED (hr)) {
            hr = E_UNEXPECTED;
            break;
        }

        // Задаем значение кольца
        put_RingValue (nIndex, vValue.lVal);
    }

    if (SUCCEEDED(hr)) m_bRequiresSave = FALSE;
    return hr;
}

Метод Save работает симметрично. Реализация этого метода есть в классе IPersistPropertyBagImpl<T>. Метод IPersistPropertyBagImpl<T>::Save вызывает IPersistPropertyBag_Save для выполнения работы. В вашем классе (T) метода IPersistPropertyBag_Save обычно нет, поэтому вызывается реализация этого метода по умолчанию, предоставляемая базовым классом IPersistPropertyBagImpl<T>. Реализация по умолчанию перебирает записи в таблице свойств и по очереди сохраняет свойство, соответствующее каждой записи.

template <class T>
class ATL_NO_VTABLE IPersistPropertyBagImpl
    : public IPersistPropertyBag {
public:
    ...
    // IPersistPropertyBag
    //
    STDMETHOD(Save)(LPPROPERTYBAG pPropBag, BOOL fClearDirty,
        BOOL fSaveAllProperties) {
        ATLTRACE(atlTraceCOM, 2, _T("IPersistPropertyBagImpl::Save\n"));
        T* pT = static_cast<T*>(this);
        ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
        ATLASSERT(pMap != NULL);
        return pT->IPersistPropertyBag_Save(pPropBag,
            fClearDirty, fSaveAllProperties, pMap);
    }

    HRESULT IPersistPropertyBag_Save(LPPROPERTYBAG pPropBag,
        BOOL fClearDirty, BOOL fSaveAllProperties,
        ATL_PROPMAP_ENTRY* pMap) {
        T* pT = static_cast<T*>(this);
        HRESULT hr;
        hr = AtlIPersistPropertyBag_Save(pPropBag, fClearDirty,
            fSaveAllProperties, pMap, pT, pT->GetUnknown());
        if (fClearDirty && SUCCEEDED(hr)) {
            pT->m_bRequiresSave=FALSE;
        }
        return hr;
    }
};

И наконец, метод InitNew в IPersistPropertyBagImpl реализован следующим образом.

STDMETHOD(InitNew)() {
    ATLTRACE(atlTraceCOM, 2,
        _T("IPersistPropertyBagImpl::InitNew\n"));
    T* pT = static_cast<T*>(this);
    pT->m_bRequiresSave = TRUE;
    return S_OK;
}

Поэтому, чтобы выполнять какую-то инициализацию, если значения свойств не загружаются, нужно переопределить InitNew напрямую.

Реализация IPersistStreamInitImpl

Реализация, содержащаяся в IPersistStreamInitImpl, весьма похожа на только что рассмотренную нами. Методы Load и Save вызывают методы IPersistStreamInit_Load и IPersistStreamInit_Save, которые могут предоставляться производным классом. Однако обычно используются реализации IPersistStreamInit_Load и IPersistStreamInit_Save из IPersistStreamInitImpl. Эти реализации вызывают глобальные вспомогательные функции AtlIPersistStreamInit_Load и AtlIPersistStreamInit_Save.

template <class T>
class ATL_NO_VTABLE IPersistStreamInitImpl
  : public IPersistStreamInit {
public:
  ...
  // IPersistStream
  STDMETHOD(Load)(LPSTREAM pStm) {
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStreamInitImpl::Load\n"));
    T* pT = static_cast<T*>(this);
    return pT->IPersistStreamInit_Load(pStm,
      T::GetPropertyMap());
  }

  HRESULT IPersistStreamInit_Load(LPSTREAM pStm,
    ATL_PROPMAP_ENTRY* pMap) {
    T* pT = static_cast<T*>(this);
    HRESULT hr =
      AtlIPersistStreamInit_Load(pStm, pMap, pT,
    pT->GetUnknown());
    if (SUCCEEDED(hr)) pT->m_bRequiresSave = FALSE;
    return hr;
  }

  STDMETHOD(Save)(LPSTREAM pStm, BOOL fClearDirty) {
    T* pT = static_cast<T*>(this);
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStreamInitImpl::Save\n"));
    return pT->IPersistStreamInit_Save(pStm, fClearDirty,
      T::GetPropertyMap());
  }

  HRESULT IPersistStreamInit_Save(LPSTREAM pStm,
    BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap) {
    T* pT = static_cast<T*>(this);
    return AtlIPersistStreamInit_Save(pStm, fClearDirty,
      pMap, pT, pT->GetUnknown());
  }
};

Метод InitNew в IPersistStreamInitImpl реализован так:

STDMETHOD(InitNew)() {
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStreamInitImpl::InitNew\n"));
    T* pT = static_cast<T*>(this);
    pT->m_bRequiresSave = TRUE;
  return S_OK;
}

Соответственно, как и при работе с пакетами свойств, вы должны переопределить метод InitNew напрямую, если нужно выполнять какую-то инициализацию объектов, когда значения свойств не загружаются.

Реализация метода IsDirty основана на предположении, что где-то в вашей иерархии классов есть переменная m_bRequiresSave.

STDMETHOD(IsDirty)() {
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStreamInitImpl::IsDirty\n"));
    T* pT = static_cast<T*>(this);
    return (pT->m_bRequiresSave) ? S_OK : S_FALSE;
}

Изначально реализации сохраняемости были рассчитаны на использование только в элементах управления ActiveX, как будто элементы управления — это единственные объекты, состояние которых нужно сохранять. Хотя ATL сильно ослабила связь между классами элементов управления и реализацией сохраняемости, класс CComControlBase обычно предоставляет переменную m_bRequiresSave и вспомогательные методы SetDirty и GetDirty, используемые для доступа к ней.

class ATL_NO_VTABLE CComControlBase {
public:
    void SetDirty(BOOL bDirty) { m_bRequiresSave = bDirty; }

    // Проверяем, требуется ли элементу управления сохранение
    BOOL GetDirty() { return m_bRequiresSave; }
    ...
    unsigned m_bRequiresSave:1;
};

Чтобы использовать классы реализации сохраняемости для объектов, классы которых не наследуют CComControlBase, нужно определить переменную m_bRequiresSave где-то в иерархии классов этих объектов. Обычно для удобства определяются также служебные методы SetDirty и GetDirty. Объекты, не являющиеся элементами управления, для обеспечения поддержки сохраняемости могут использовать такой класс.

class ATL_NO_VTABLE CSupportDirtyBit {
public:
  CSupportDirtyBit() : m_bRequiresSave(FALSE) {}
  void SetDirty(BOOL bDirty) {
    m_bRequiresSave = bDirty ? TRUE : FALSE;
  }
  BOOL GetDirty() { return m_bRequiresSave ? TRUE : FALSE; }
  BOOL m_bRequiresSave;
};

И наконец, IPersistStreamInitImpl предоставляет следующую реализацию метода GetSizeMax.

STDMETHOD(GetSizeMax)(ULARGE_INTEGER* pcbSize) {
  HRESULT hr = S_OK;
  T* pT = static_cast<T*>(this);

  if (pcbSize == NULL)
    return E_POINTER;

  ATL_PROPMAP_ENTRY* pMap = T::GetPropertyMap();
  ATLENSURE(pMap != NULL);

  // В качестве начального возьмем размер ATL-версии,
  // созданной нами.
  ULARGE_INTEGER nSize;
  nSize.HighPart = 0;
  nSize.LowPart = sizeof(DWORD);

  CComPtr<IDispatch> pDispatch;
  const IID* piidOld = NULL;
  for (int i = 0; pMap[i].pclsidPropPage != NULL; i++) {
    if (pMap[i].szDesc == NULL)
      continue;

    // проверяем, соответствуют ли записи чистые данные
    if (pMap[i].dwSizeData != 0) {
      ULONG ulSize=0;
      // Вычисляем размер потока для особого случая - BSTR
      if (pMap[i].vt == VT_BSTR) {
        void* pData = (void*)(pMap[i].dwOffsetData +
          (DWORD_PTR)pT);
        ATLENSURE(
          pData >= (void*)(DWORD_PTR)pMap[i].dwOffsetData
            && pData >= (void*)(DWORD_PTR)pT );
          BSTR bstr=*reinterpret_cast<BSTR*>(pData);
          ulSize=CComBSTR::GetStreamSize(bstr);
        } else {
          ulSize = pMap[i].dwSizeData;
        }
        nSize.QuadPart += ulSize;
        continue;
      }

      CComVariant var;
      if (pMap[i].piidDispatch != piidOld) {
        pDispatch.Release();
        if (FAILED(pT->GetUnknown()->
          QueryInterface(*pMap[i].piidDispatch,
          (void**)&pDispatch))) {
          ATLTRACE(atlTraceCOM, 0,
            _T("Failed to get a dispatch pointer for "
               "property #%i\n"), i);
          hr = E_FAIL;
          break;
        }
        piidOld = pMap[i].piidDispatch;
      }

      if (FAILED(pDispatch.GetProperty(pMap[i].dispid, &var))) {
        ATLTRACE(atlTraceCOM, 0,
          _T("Invoked failed on DISPID %x\n"),
          pMap[i].dispid);
        hr = E_FAIL;
        break;
    }
    nSize.QuadPart += var.GetSize();
  }
  *pcbSize = nSize;
  return hr;
}

Предыдущие версии ATL просто возвращали значение E_NOTIMPL из метода GetSizeMax. На момент написания этой книги MSDN утверждала, что этот метод все еще не реализован в ATL. В любом случае реализация, которую предлагает ATL 8, весьма прямолинейна. GetSizeMax перебирает все записи в таблице свойств и суммирует размеры этих свойств в переменной nSize. Если он находит соответствующую “чистым” данным запись PROP_DATA_ENTRY, он просто увеличивает значение nSize на размер переменной, указанной в макросе PROP_DATA_ENTRY. Если он находит запись PROP_ENTRY или PROP_ENTRY_EX, он запрашивает у объекта указанный интерфейс IDispatch и заворачивает полученный интерфейсный указатель в объект CComPtr. Вспомните — в главе 3, “Интеллектуальные типы ATL”, говорилось, что класс интеллектуальных указателей CComPtr предоставляет удобную специализацию IDispatch, позволяющую работать с методами чтения и записи значений. GetSizeMax использует метод CComPtr<IDispatch>::GetProperty для получения значения типа VARIANT для свойства, указанного в записи таблицы свойств. Он заворачивает полученное значение типа VARIANT в объект класса CComVariant и увеличивает значение переменной nSize на значение, возвращаемое методом GetSize этого класса.

Реализация IPersistStorageImpl

Реализация IPersistStorage, предлагаемая ATL, очень проста. Метод Save создает поток с именем "Contents" в заданном хранилище, в который и записывает данные с помощью реализации IPersistStreamInit.

template <class T>
class ATL_NO_VTABLE IPersistStorageImpl
    : public IPersistStorage {
public:
    STDMETHOD(Save)(IStorage* pStorage, BOOL fSameAsLoad) {
        ATLTRACE(atlTraceCOM, 2,
          _T("IPersistStorageImpl::Save\n"));
        CComPtr<IPersistStreamInit> p;
        p.p = IPSI_GetIPersistStreamInit();
        HRESULT hr = E_FAIL;
        if (p != NULL) {
            CComPtr<IStream> spStream;
            static LPCOLESTR vszContents = OLESTR("Contents");
            hr = pStorage->CreateStream(vszContents,
              STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE,
              0, 0, &spStream);
            if (SUCCEEDED(hr)) hr = p->Save(spStream, fSameAsLoad);
        }
        return hr;
    }
    ...
};

Метод Load открывает поток "Contents" и с помощью реализации IPersistStreamInit загружает содержимое потока.

STDMETHOD(Load)(IStorage* pStorage) {
    ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::Load\n"));
    CComPtr<IPersistStreamInit> p;
    p.p = IPSI_GetIPersistStreamInit();
    HRESULT hr = E_FAIL;
    if (p != NULL) {
        CComPtr<IStream> spStream;
        hr = pStorage->OpenStream(OLESTR("Contents"), NULL,
          STGM_DIRECT | STGM_SHARE_EXCLUSIVE, 0, &spStream);
    if (SUCCEEDED(hr)) hr = p->Load(spStream);
  }
  return hr;
}

Реализации методов InitNew и IsDirty извлекают интерфейсный указатель IPersistStreamUnit объекта (используя вспомогательную функцию для получения этого интерфейса) и вызывают методы этого интерфейса с аналогичными именами.

STDMETHOD(IsDirty)(void) {
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStorageImpl::IsDirty\n"));
    CComPtr<IPersistStreamInit> p;
    p.p = IPSI_GetIPersistStreamInit();
    return (p != NULL) ? p->IsDirty() : E_FAIL;
}

STDMETHOD(InitNew)(IStorage*) {
    ATLTRACE(atlTraceCOM, 2,
      _T("IPersistStorageImpl::InitNew\n"));
    CComPtr<IPersistStreamInit> p;
    p.p = IPSI_GetIPersistStreamInit();
    return (p != NULL) ? p->InitNew() : E_FAIL;
}

Одна из основных причин реализации IPersistStorage — возможность инкрементного считывания и записи. К несчастью, реализация, предоставляемая ATL, не позволяет этого делать. Она не кеширует интерфейсный указатель IStorage, предоставляемый во время вызовов методов Load и Save, поэтому этот указатель будет недоступен для последующих инкрементных операций чтения и записи. Однако отсутствие кеширования интерфейса IStorage делает тривиальной задачу реализации последних двух методов.

STDMETHOD(SaveCompleted)(IStorage* /* pStorage */) {
  ATLTRACE(atlTraceCOM, 2,
    _T("IPersistStorageImpl::SaveCompleted\n"));
  return S_OK;
}

STDMETHOD(HandsOffStorage)(void) {
  ATLTRACE(atlTraceCOM, 2, _T("IPersistStorageImpl::HandsOffStorage\n"));
  return S_OK;
}

Чаще всего объектам, которым требуется функциональность интерфейса IPersistStorage, не подходит реализация, предоставляемая ATL. Им приходится явно наследовать IPersistStorage и реализовывать методы самостоятельно.

Дополнительные реализации сохраняемости

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

Интерфейс IPersistMemory

Давайте рассмотрим реализацию интерфейса IPersistMemory.

interface IPersistMemory : IPersist {
    HRESULT IsDirty();
    HRESULT Load([in, size_is(cbSize)] LPVOID pvMem,
        [in] ULONG cbSize);
    HRESULT Save([out, size_is(cbSize)] LPVOID pvMem,
        [in] BOOL fClearDirty, [in] ULONG cbSize);
    HRESULT GetSizeMax([out] ULONG* pCbSize);
    HRESULT InitNew();
};

Интерфейс IPersistMemory позволяет клиенту запрашивать сохранение состояния объекта в блок памяти фиксированного размера, на который указывает указатель void*. Этот интерфейс очень похож на IPersistStreamInit, за исключением того, что он использует блок памяти вместо расширяемого потока IStream. Аргумент cbSize методов Load и Save указывает объем памяти, доступный через pvMem. Методы IsDirty, GetSizeMax и InitNew семантически идентичны представленным в IPersistStreamInit.

Реализация интерфейса IPersistMemory

Вы видели, что ATL предоставляет класс IPersistStreamInitImpl, сохраняющий состояние объектов в потоке и восстанавливающий это состояние из потока. Функция API COM CreateStreamOnHGlobal возвращает реализацию IStream, выполняющую чтение и запись в глобальном блоке памяти. Мы можем использовать эти класс и функцию и легко реализовать IPersistMemory с помощью функциональности, предоставляемой IPersistStreamInitImpl.

За исключением методов Load и Save все методы в нашей реализации IPersistMemory просто вызывают аналогичные методы в предоставляемой ATL реализации IPersistStreamInit.

template <class T, class S = IPersistStreamInit>
class ATL_NO_VTABLE IPersistMemoryImpl : public IPersistMemory {
public:
    // IPersist
    STDMETHODIMP GetClassID(CLSID *pClassID) {
        ATLTRACE(atlTraceCOM, 2, _T("IPersistMemoryImpl::GetClassID\n"));
        T* pT = static_cast<T*>(this);
        S* psi = static_cast <S*> (pT);
        return psi->GetClassID(pClassID);
    }

    // IPersistMemory
    STDMETHODIMP IsDirty() {
        ATLTRACE(atlTraceCOM, 2,
        _T("IPersistMemoryImpl::IsDirty\n"));
        T* pT = static_cast<T*>(this);
        S* psi = static_cast <S*> (pT);
        return psi->IsDirty();
    }

    STDMETHODIMP Load(void* pvMem, ULONG cbSize) {
        ATLTRACE(atlTraceCOM, 2,
            _T("IPersistMemoryImpl::Load\n"));
        T* pT = static_cast<T*>(this);
        // Получаем дескриптор блока памяти. Нам нужен настоящий HGLOBAL,
        // потому что он нужен для CreateStreamOnHGlobal
        HGLOBAL h = GlobalAlloc(GMEM_MOVEABLE, cbSize);
        if (h == NULL) return E_OUTOFMEMORY;
        LPVOID pv = GlobalLock(h);
        if (!pv) return E_OUTOFMEMORY;

        // Копирование в блок памяти
        CopyMemory (pv, pvMem, cbSize);
        CComPtr<IStream> spStrm;
        // Создаем поток в памяти
        HRESULT hr = CreateStreamOnHGlobal (h, TRUE, &spStrm);
        if (FAILED (hr)) {
            GlobalUnlock (h);
            GlobalFree (h);
            return hr;
        }
        // Теперь память принадлежит потоку

        // Загружаем данные из потока
        S* psi = static_cast <S*> (pT);
        hr = psi->Load (spStrm);

        GlobalUnlock (h);
        return hr;
    }

    STDMETHODIMP Save(void* pvMem, BOOL fClearDirty,
        ULONG cbSize) {
        ATLTRACE(atlTraceCOM, 2,
            _T("IPersistMemoryImpl::Save\n"));
        T* pT = static_cast<T*>(this);

        // Получаем дескриптор памяти
        HGLOBAL h = GlobalAlloc (GMEM_MOVEABLE, cbSize);
        if (NULL == h) return E_OUTOFMEMORY;

        // Создаем в памяти поток
        CComPtr<IStream> spStrm;
        HRESULT hr = CreateStreamOnHGlobal (h, TRUE, &spStrm);
        if (FAILED (hr)) {
            GlobalFree (h);
            return hr;
        }
        // Теперь память принадлежит потоку
        // Задаем логический размер потока равным физическому размеру
        // блока памяти (округления при выделении глобальных блоков могут
        // вызвать различия в размерах)
        ULARGE_INTEGER uli;
        uli.QuadPart = cbSize ;
        spStrm->SetSize (uli);

        S* psi = static_cast <S*> (pT);
        hr = psi->Save (spStrm, fClearDirty);
        if (FAILED (hr)) return hr;

        LPVOID pv = GlobalLock (h);
        if (!pv) return E_OUTOFMEMORY;

        // Копируем в блок памяти
        CopyMemory (pvMem, pv, cbSize);

        return hr;
    }

    STDMETHODIMP GetSizeMax(ULONG* pcbSize) {
        if (pcbSize == NULL) return E_POINTER;
        *pcbSize = 0;

        T* pT = static_cast<T*>(this);
        S* psi = static_cast <S*> (pT);
        ULARGE_INTEGER uli ;
        uli.QuadPart = 0;
        HRESULT hr = psi->GetSizeMax (&uli);
        if (SUCCEEDED (hr)) *pcbSize = uli.LowPart;

        return hr;
    }

    STDMETHODIMP InitNew() {
        ATLTRACE(atlTraceCOM, 2,
            _T("IPersistMemoryImpl::InitNew\n"));
        T* pT = static_cast<T*>(this);
        S* psi = static_cast <S*> (pT);
        return psi->InitNew();
    }
};

Обратите внимание на использование static_cast для преобразования указателя this в указатель на производный класс и преобразование полученного указателя в IPersistStreamInit*. Выполняя это преобразование, мы получим ошибку при компиляции, если класс, наследующий IPersistMemoryImpl, не наследует IPersistStreamInit. Такой подход требует от производного класса не реализовывать IPersistStreamInit “необычным” способом, например, через отделяемый интерфейс или агрегирование.

В качестве альтернативы можно было бы получить интерфейс IPersistStreamInit с помощью QueryInterface.

T* pT = static_cast<T*>(this);
CComQIPtr<S> psi = pT->GetUnknown() ;

Однако на этапе выполнения мы можем обнаружить, что реализация IPersistStreamInit недоступна, а это значит, что объект будет сообщать, будто он реализует интерфейс IPersistMemory, хотя на самом деле он не сможет его реализовать. Я предпочитаю ошибки во время компиляции отказам во время выполнения программ, поэтому я предпочту первый способ второму.

Использование шаблона IPersistMemoryImpl

Объекты могут использовать нашу реализацию IPersistMemory следующим образом.

class ATL_NO_VTABLE CDemagogue :
    ...
    public IPersistStreamInitImpl<CDemagogue>,
    public IPersistMemoryImpl<CDemagogue>,
    public CSupportDirtyBit {
    ...
BEGIN_COM_MAP(CDemagogue)
    ...
    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    COM_INTERFACE_ENTRY(IPersistMemory)
END_COM_MAP()

Добавление семантики маршалинга по значению с помощью сохраняемости

Когда вы передаете интерфейсный указатель как параметр при удаленном (за пределами апартамента) вызове метода, COM по умолчанию передает этот параметр по ссылке. Другими словами, объект остается там, где и был, и принимающему вызов методу передается только ссылка на этот объект. Это чаще всего означает, что обращения к объекту становятся весьма накладными. Объект может изменить такой выбор по умолчанию, реализовав интерфейс IMarshal.

Чаще всего разработчики реализуют IMarshal в своих объектах, чтобы разрешить передачу по значению. Другими словами, когда вы передаете интерфейсный указатель при удаленном вызове метода, вы заставляете COM передать методу копию объекта. Все обращения к этому объекту станут локальными и не будут требовать обращения к “исходному” объекту. Когда объект реализует IMarshal так, что он обеспечивает передачу по значению, мы обычно говорим, что для объекта выполняется маршалинг по значению.

interface IMarshal : public IUnknown {
  STDMETHOD GetUnmarshalClass([in] REFIID riid,
    [unique,in] void* pv,
    [in] DWORD dwDestContext,
    [unique,in] void* pvDestContext,
    [in] DWORD mshlflags, [out] CLSID* pCid);

  STDMETHOD GetMarshalSizeMax([in] REFIID riid,
    [unique,in] void* pv,
    [in] DWORD dwDestContext,
    [unique,in] void* pvDestContext,
    [in] DWORD mshlflags,
    [out] DWORD* pSize) ;

  STDMETHOD MarshalInterface([unique,in] IStream* pStm,
    [in] REFIID riid, [unique][in] void* pv,
    [in] DWORD dwDestContext,
    [unique,in] void* pvDestContext,
    [in] DWORD mshlflags);

  STDMETHOD UnmarshalInterface([unique,in] IStream* pStm,
    [in] REFIID riid, [out] void** ppv);

  STDMETHOD ReleaseMarshalData([unique,in] IStream* pStm);
  STDMETHOD DisconnectObject([in] DWORD dwReserved);
};

С помощью полной реализации IPersistStream или IPersistStreamInit создать реализацию IMarshal, позволяющую маршалинг по значению, несложно.

Класс обычно реализует маршалинг по значению, возвращая собственный CLSID как результат вызова метода GetUnmarshalClass. Требуемый CLSID получается с помощью метода IPersistStream::GetClassID.

Метод GetMarshalSizeMax должен возвращать количество байтов, необходимое для сохранения состояния объекта в потоке. Это количество получается с помощью метода IPersistStream::GetSizeMax.

Методы MarshalInterface и UnmarshalInterface должны соответственно записывать в указанный поток и считывать из него состояние объекта. Для этого мы можем использовать методы Save и Load интерфейса IPersistStream.

Методы ReleaseMarshalData и DisconnectObject могут просто возвращать значение S_OK.

Вот класс-шаблон, использующий интерфейс IPersistStreamInit объекта для реализации маршалинга по значению. В этой реализации мы тоже используем приведение типов с помощью static_cast для получения интерфейса IPersistStreamInit, поэтому мы получим сообщение об ошибке на этапе компиляции, если производный класс не реализует интефейс IPersistStreamInit.

template <class T>
class ATL_NO_VTABLE IMarshalByValueImpl : public IMarshal {

  STDMETHODIMP GetUnmarshalClass(REFIID /* riid */,
    void* /* pv */,
    DWORD /* dwDestContext */,
    void* /* pvDestContext */,
    DWORD /* mshlflags */, CLSID *pCid) {
    T* pT = static_cast<T*>(this);
    IPersistStreamInit* psi =
      static_cast<IPersistStreamInit*>(pT);
    return psi->GetClassID (pCid);
  }

  STDMETHODIMP GetMarshalSizeMax(REFIID /* riid */,
    void* /* pv */,
    DWORD /* dwDestContext */,
    void* /* pvDestContext */,
    DWORD /* mshlflags */, DWORD* pSize) {
    T* pT = static_cast<T*>(this);
    IPersistStreamInit* psi =
      static_cast <IPersistStreamInit*> (pT);

    ULARGE_INTEGER uli = { 0 };

    HRESULT hr = psi->GetSizeMax(&uli);
    if (SUCCEEDED (hr)) *pSize = uli.LowPart;
    return hr;
  }

  STDMETHODIMP MarshalInterface(IStream *pStm, REFIID /* riid */,
    void* /* pv */, DWORD /* dwDestContext */,
    void* /* pvDestCtx */, DWORD /* mshlflags */) {
    T* pT = static_cast<T*>(this);
    IPersistStreamInit* psi = static_cast <IPersistStreamInit*> (pT);
    return psi->Save(pStm, FALSE);
  }

  STDMETHODIMP UnmarshalInterface(IStream *pStm, REFIID riid,
    void **ppv) {
    T* pT = static_cast<T*>(this);
    IPersistStreamInit* psi =
      static_cast <IPersistStreamInit*> (pT);
    HRESULT hr = psi->Load(pStm);
    if (SUCCEEDED (hr)) hr = pT->QueryInterface (riid, ppv);
    return hr;
  }

  STDMETHODIMP ReleaseMarshalData(IStream* /* pStm */) {
    return S_OK;
  }

  STDMETHODIMP DisconnectObject(DWORD /* dwReserved */) {
    return S_OK;
  }
};

Этот шаблон позволяет придать объектам способность маршалинга по значению. Для этого нужно сделать ваш класс производным от IMarshalByValueImpl (чтобы получить реализации методов IMarshal) и IPersistStreamInitImpl. Кроме того, нужно добавить запись COM_INTERFACE_ENTRY для IMarshal в таблицу интерфейсов класса. Вот образец.

class ATL_NO_VTABLE CDemagogue :
    ...
    public IPersistStreamInitImpl<CDemagogue>,
    public CSupportDirtyBit,
    public IMarshalByValueImpl<CDemagogue> {
    ...
  BEGIN_COM_MAP(CDemagogue)
    COM_INTERFACE_ENTRY2(IPersist, IPersistStreamInit)
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    COM_INTERFACE_ENTRY(IMarshal)
  END_COM_MAP()
...
};

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

Резюме

Поддержка сохраняемости в той или иной форме требуется многим объектам, и ATL предоставляет легко расширяемые реализации интерфейсов IPersistStream, IPersistStreamInit и IPersistPropertyBag, основанные на таблицах свойств. Эти реализации сохраняют и загружают значения свойств соответственно записям в таблицах свойств классов. Переопределяя соответствующие методы, можно легко расширить механизмы сохраняемости, обеспечив поддержку типов данных, не поддерживаемых записями в таблицах свойств. Реализация IPersistStorage в ATL позволяет объектам встраиваться в контейнеры OLE, но не дает возможности пользоваться многими из преимуществ носителей IStorage.

Использование и расширение потоковых механизмов сохраняемости, предоставляемых ATL, позволяет объекту предлагать клиентам дополнительную функциональность. Например, вы уже видели, как можно реализовывать поддержку IPersistMemory в ваших объектах (именно это и предпочтительно для контейнеров, основанных на MFC). Кроме того, с помощью потоковых механизмов сохраняемости можно легко добавлять в классы возможности маршалинга по значению.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 0    Оценка 0 [+0/-1]         Оценить