Продолжается подписка на наши издания! Вы не забыли подписаться?

VARIANT и SafeArray

В Internet-конференциях часто задаются вопросы, связанные с использованием таких типов, как VARIANT, SafeArray, BSTR и им подобных. Эти типы и API для работы с ними появились еще в 1995 году. Microsoft уже заявляет об отказе от развития COM, а количество вопросов не уменьшается.

Уже в те незапамятные годы автор воистину великого труда Hardcore Visual Basic (кто не читал, настоятельно рекомендуем, несмотря на отвратительное качество русского перевода, даже название было переведено как «Visual Basic – крепкий орешек», до 2000 года она входила в состав MSDN) написал ряд статей, посвященных этому вопросу. Но Automation API тех времен и современное – это совершенно разные вещи. Долгое время по Automation API даже не существовало человеческой документации, а многие функции просто не были документированы. К началу нового столетия Microsoft накопил денег, и посадил технических писателей за документацию Automation API. К сожалению, они слишком близко приняли к сердцу изречение А.П.Чехова «краткость – сестра таланта», и воплотили его в жизнь в этой документации чересчур буквально.

Очевидно, назрела насущная необходимость в новой версии такого материала. Свою статью я решил построить похожим образом.

VARIANT

С давних пор интерпретаторы пользовались нетипизированными переменными. О том, что это плохо, сказано уже достаточно, но в нетипизированных переменных были и свои несомненные плюсы. Идея, что все типы могут содержаться в одном, является компромиссом между производительностью, размером и безопасностью программирования – с одной стороны, и гибкостью и простотой использования – с другой. Basic на пути от встроенного в BIOS интерпретатора до последней версии VB прошел путь от нетипизированного до строго типизированного языка. Так как разработчики пытались сохранить основное свойство языка – легкость программирования и пригодность для , они вынуждены были ввести универсальный тип – Variant. Это случилось в середине 90-х годов, когда появился новый для тех времен тип Variant. Так как весь VB был переписан на COM, то и Variant стал частью COM API. Variant трансформировался из частной возможности VB в стандартный COM-тип. Сегодня Variant является частью так называемого OLE Automation API. Поддержка Variant была встроена в большинство средств разработки для Windows – или как встроенного типа, или класса-обертки. Так, он появился в Delphi, начиная с версии 2.0 (как внутренний тип), в Microsoft VC++ (COleVariant – MFC, CComVariant – ATL), и т.п.

Что такое VARIANT?

По сути, у Variant-а есть три ипостаси – variant, VARIANT и Variant.

VARIANT изнутри

Без этого не обойтись. Придется разобраться со структурой VARIANT, прежде, чем что-то делать с ней. По большому счету, Variant – это большой union, заключенный в структуру. Если вдаваться в подробности, это несколько вложенных структур и union-ов, но пока это не важно. Вот как это выглядит:

struct tagVARIANT          
{                          
  union                    Объединение для типа decimal
  {                        
    struct __tagVARIANT    
    {                      
      VARTYPE vt;          поле, определяющее хранимый тип (VT_-тип)
      WORD wReserved1;     
      WORD wReserved2;     
      WORD wReserved3;     
      union                
      {                    
                                VT_-тип (С/C++)            Тип VB
        LONGLONG llVal;         VT_I8                     н/п
        LONG lVal;              VT_I4                     ByVal Long
        BYTE bVal;              VT_UI1                    ByVal Byte
        SHORT iVal;             VT_I2                     ByVal Integer
        FLOAT fltVal;           VT_R4                     ByVal Single
        DOUBLE dblVal;          VT_R8                     ByVal Double
        VARIANT_BOOL boolVal;   VT_BOOL                   ByVal Boolean
        _VARIANT_BOOL bool;                               --
        SCODE scode;            VT_ERROR                  Missing
        CY cyVal;               VT_CY                     ByVal Currency
        DATE date;              VT_DATE                   ByVal Date
        BSTR bstrVal;           VT_BSTR                   ByVal String
        IUnknown *punkVal;      VT_UNKNOWN                ByVal Object
        IDispatch *pdispVal;    VT_DISPATCH               ByVal Object
        SAFEARRAY *parray;      VT_ARRAY | *              Массив
                                                          (без типа н/п)
        BYTE *pbVal;            VT_BYREF|VT_UI1           ByRef Long
        SHORT *piVal;           VT_BYREF|VT_I2            ByRef Byte
        LONG *plVal;            VT_BYREF|VT_I4            ByRef Integer
        LONGLONG *pllVal;       VT_BYREF|VT_I8            ByRef Single
        FLOAT *pfltVal;         VT_BYREF|VT_R4            ByRef Double
        DOUBLE *pdblVal;        VT_BYREF|VT_R8            ByRef Boolean
        VARIANT_BOOL *pboolVal; VT_BYREF|VT_BOOL          ByRef Boolean
        _VARIANT_BOOL *pbool;                             
        SCODE *pscode;          VT_BYREF|VT_ERROR         н/п
        CY *pcyVal;             VT_BYREF|VT_CY            ByRef Currency
        DATE *pdate;            VT_BYREF|VT_DATE          ByRef Date
        BSTR *pbstrVal;         VT_BYREF|VT_BSTR          ByRef String
        IUnknown **ppunkVal;    VT_BYREF|VT_UNKNOWN       ByRef Object
        IDispatch **ppdispVal;  VT_BYREF|VT_DISPATCH      ByRef Object
        SAFEARRAY **pparray;    VT_BYREF|VT_ARRAY         ByRef Массив 
                                                          (без типа н/п)

        VARIANT *pvarVal;       VT_BYREF|VT_VARIANT       ByRef Variant
        PVOID byref;            VT_BYREF (Generic ByRef)  н/п
        CHAR cVal;              VT_I1                     н/п
        USHORT uiVal;           VT_UI2                    н/п
        ULONG ulVal;            VT_UI4                    н/п
        ULONGLONG ullVal;       VT_UI8                    н/п
        INT intVal;             VT_INT                    н/п
        UINT uintVal;           VT_UINT                   н/п
        DECIMAL *pdecVal;       VT_BYREF|VT_DECIMAL       Только в Variant-е
        CHAR *pcVal;            VT_BYREF|VT_I1            н/п
        USHORT *puiVal;         VT_BYREF|VT_UI2           н/п
        ULONG *pulVal;          VT_BYREF|VT_UI4           н/п
        ULONGLONG *pullVal;     VT_BYREF|VT_UI8           н/п
        INT *pintVal;           VT_BYREF|VT_INT           н/п
        UINT *puintVal;         VT_BYREF|VT_UINT          н/п
        struct __tagBRECORD     VT_RECORD (структура)     ByRef Type
        {                       
          PVOID pvRecord;           Указатель на структуру
          IRecordInfo *pRecInfo;    Описание структуры.
        }   __VARIANT_NAME_4; 
      }   __VARIANT_NAME_3;   
    }   __VARIANT_NAME_2;     
    DECIMAL decVal;           
  } __VARIANT_NAME_1;         
};                            

Идея в том, что union может содержать любой из поддерживаемых типов. Поле VARTYPE vt должно содержать тег типа (и, возможно, модификатор тега VT_ARRAY или VT_BYREF), указывающий, что именно содержит union. Технически это все, что действительно нужно знать. Каждый раз, когда в VARIANT помещаются данные, нужно задать их тип. Каждый раз, когда данные извлекаются, необходимо определить их тип, чтобы знать, как их обрабатывать. На самом деле COM предоставляет группу системных функций, помогающих справиться с этими задачами. Вскоре мы перейдем к их описаниям.

Реально существуют несколько модификаторов тегов типа данных, но действительно используются два (в скобках приведены их значения, так как они являются битовыми флагами):

VT_ARRAY (0x2000) – указывает, что в Variant хранится массив в виде SAFEARRAY, о котором речь пойдет ниже. При этом с помощью операции «или», т.е. «|», задается тип элемента SAFEARRAY. При этом могут храниться массивы следующих типов: VT_I1, VT_UI1, VT_I2, VT_UI2, VT_I4, VT_UI4, VT_INT, VT_UINT, VT_R4, VT_R8, VT_BOOL, VT_DECIMAL, VT_ERROR, VT_CY, VT_DATE и VT_BSTR. Необходимо учитывать, что VB будет воспринимать только массивы, типы данных которых ему понятны.

VT_BYREF (0x4000) – указывает, что в Variant хранится ссылка (указатель) на переменную, тип которой задается с помощью операции «или», т.е. «|». В описании структуры Variant эти поля помечены как VT_BYREF| *, где * – это некоторый тип данных.

Как велик VARIANT?

Первое поле – VARTYPE, которое на самом деле является typedef-ом для unsigned short. Следующие три поля – WORD, еще один typedef для unsigned short. Итак, VARIANT – это 8 байт плюс размер union. Union содержит достаточно места для своего самого большого члена, в данном случае это 8 байт (для double, currency или date). Итак, VARIANT – это 16 байт. Однако есть одно исключение. В Variant поддерживается один забавный тип – DECIMAL. Вот его описание:

struct tagDEC
{
  USHORT wReserved;
  union
  {
    struct
    {
      BYTE scale;
      BYTE sign;
    };
    USHORT signscale;
  };
  ULONG Hi32;
  union
  {
    struct
    {
       ULONG Lo32;
       ULONG Mid32;
    };
    ULONGLONG Lo64;
  };
} DECIMAL;

Нетрудно посчитать, что размер этого типа – 16 байт. Как же он умудряется умещаться в VARIANT, размер которого тоже 16 байт? Обратите внимание на выделенное жирным поле wReserved. Его тип – USHORT – это еще один typedef для unsigned short. :) Если в Variant помещается DECIMAL, в это поле незаконно помещается значение поля vt из VARIANT.

Конечно, находчивости программистов Microsoft можно только позавидовать, но, тем не менее, 16 байт – это большой перерасход места, если взять, к примеру, массив VARIANT-ов, содержащих по 1 байту (VT_UI1). К счастью, столь явные перекосы случаются не так часто. На 32-битных платформах наиболее часто используемый размер данных – 4 байта (для long, ошибки или указателя любого сорта). В таком случае всего лишь 11 из 16 байт пропадают впустую.

Зачастую в коде или описании методов можно встретить тип VARIANTARG. Реально VARIANTARG – не более, чем typedef для VARIANT, но, так же, как BSTR – это больше, чем его typedef, VARIANTARG – это больше, чем VARIANT. Сказать точнее, VARIANTARG включает все, что есть в VARIANT, но в VARIANT есть не все, что включает VARIANTARG. Или, чтобы уменьшить путаницу, VARIANTARG – это VARIANT, используемый как аргумент IDispatch и может содержать все из union VARIANT-а, включая ссылочные аргументы. В других контекстах VARIANT не предполагает использования всех этих членов union, помеченных VT_BYREF во второй части union.

Интерфейс IDispatch – это увлекательная вещь, но здесь речь не о нем. Можете забыть о VARIANTARG и всех by-reference полях в VARIANT union.

VARIANT API

До последнего времени документация по Automation API была полным убожеством, и пользоваться ей было практически невозможно. К счастью, Microsoft нашел деньги на разделы MSDN, посвященные этому вопросу, правда, и тут не обошлось без ляпсусов.

VOID VariantInit(VARIANTARG * pvarg);

Эта функция инициализирует VARIANT. Инициализация заключается в том, что поле vt структуры VARIANT устанавливается в VT_EMPTY. При этом все остальное содержимое структуры остается нетронутым. Эта функция великолепна как по функциональности, так и по законченности концептуального решения. Жаль только времени, расходуемого на ее использование, поскольку такой системный подход выливается в очередное замедление и без того не быстрых приложений, так как вместо выполнения одной инструкции (которую, по большому счету, можно и не выполнять) приходится выполнять целое море. Вызов этой функции:

VariantInit(&varSrc);

Аналогичен следующему коду:

varSrc.vt = VT_EMPTY;
HRESULT VariantClear(VARIANT * pvarg);

Эта функция очищает VARIANT. Заметьте, что VariantClear радикально отличается от VariantInit, но только если в VARIANT-е содержатся сложные ссылочные типы данных: BSTR, SAFEARRAY, структуры или указатель на интерфейс. Она вызывает соответствующую типу функцию для освобождения этих данных, после чего аналогично VariantInit записывает VT_EMPTY. Неопытные программисты, видя схожесть VariantInit и VariantClear, могут случайно или намеренно использовать вместо одной функции другую. Это может работать достаточно долго, но в конце концов порождает тщательно камуфлированные грабли, ручка которых отлично отпечатывается на лбу, а спусковой механизм найти так же трудно, как проход по памяти.

HRESULT VariantCopy(VARIANT * pvDst, VARIANT * pvSrc);

Эта функция копирует один VARIANT в другой. Если VARIANT, в который производится копирование (pvDst), уже содержит какие-либо данные, перед копированием они освобождаются вызовом VariantClear. В результате получаются два идентичных VARIANT-а. Если источник содержит сложный ссылочный тип данных (BSTR, SAFEARRAYили структуры), у результата будет собственная идентичная копия. Если источник содержит указатель на интерфейс, счетчик ссылок будет увеличен. При этом оба VARIANT-а будут указывать на одну копию объекта.

Если копируемый Variant – это COM-объект, передаваемый по ссылке, поле vt параметра pvargSrc имеет значение VT_DISPATCH | VT_BYREF или VT_UNKNOWN | VT_BYREF. В этом случае VariantCopy не увеличивает счетчик ссылок объекта, на который производится ссылка. Поскольку копируемый Variant – это указатель на ссылку на объект, у VariantCopy нет способа определить, нужно ли увеличивать счетчик ссылок объекта. Вызывать для объекта IUnknown::AddRef или нет – на совести разработчика.

Пример:

VARIANT varDst; 
// Обратите внимание – без следующей строчки VariantClear будет вызван 
// для случайных данных, что приведет к проходу по памяти!
varDst.vt = VT_EMPTY; 
hres = VariantCopy(&varDst, &varSrc); 
HRESULT VariantCopyInd(VARIANT * pvDst, 
VARIANT * pvSrc);

Эта функция копирует один вариант в другой, примерно так же, как VariantCopy, но при этом гарантирует, что результат содержит копии значений данных (ByValue), даже если источник содержит ссылочные данные (ByRef). Например, если исходный VARIANT содержит VT_BYREF | VT_I2, приемник будет содержать VT_I2. Причем копирование осуществляется, даже если источник содержал VT_BYREF | VT_VARIANT.

HRESULT VariantChangeType(VARIANT * pvDst, 
VARIANT * pvSrc, WORD wFlags, VARTYPE vt);

Эта функция изменяет тип, содержащийся в VARIANT. Эта функция может как конвертировать значение в одной и той же переменной (по месту), так и создавать копию. В первом случае в качестве указателя на источник и приемник нужно указать адрес одной и той же переменной. Перед присвоением нового значения переменная, указанная в pvDst, предварительно очищается с помощью VariantClear. Поэтому, как и в случае VariantCopy, новую переменную необходимо инициализировать.

Параметр wFlags может принимать одно из следующих значений:

Пример:

varSrc.dblVal = 3.1416
hr = VariantChangeType(&varDst, &varSrc, 0, VT_I4)
if(hr)
 return hr;
hres = VariantChangeType(&varSrc, &varSrc, 0, VT_BSTR)
if (hr)
  return hr;
HRESULT  VariantChangeTypeEx(VARIANT * pvDest, 
VARIANT * pvSrc, LCID lcid, 
WORD wFlags, VARTYPE vt); 

Эта функция – то же самое, что VariantChangeType, за исключением параметра LCID, где можно указать ID языка. LCID полезен при преобразованиях VT_BSTR, VT_DISPATCH и VT_DATE. Например, можно получить результат на русском языке, даже если для системы, процесса и потока указан китайский.

HRESULT VarFormat(LPVARIANT pvarIn, 
LPOLESTR pbstrFormat, int iFirstDay, 
int iFirstWeek, ULONG dwFlags, 
BSTR *pbstrOut);

Переводит Variant в строку, используя в качестве формата строку, передаваемую в параметре pbstrFormat.

Эта функция использует локаль пользователя при вызове VarTokenizeFormatString и VarFormatFromTokens. Используя эти функции по отдельности, можно поднять производительность приложения за счет исключения повтора разбора строки формата.

Если вы разрабатываете приложение для мусульманских стран, можно в параметре wFlags использовать флаг VAR_CALENDAR_HIJRI – чтобы вывести дату в привычном для пользователя формате. Следующий пример конвертирует Variant, в котором находится значение типа double, в строку с разделением на декады и обязательными двумя цифрами после точки.

VARIANT var; 
var.vt = VT_R8;
var.dblVal = 123456789.12;
BSTR bsResult;
VarFormat(&var, OLESTR("### ### ### ###.00"), 0, 0, 0, bsResult);

И куча других

Функции VarFormatCurrency, VarFormatDateTime, VarFormatNumber и VarFormatPercent занимаются преобразованием в форматы, соответствующие их названиям. Эти функции используют текущие региональные установки и довольно просты в применении. Зачастую они удобнее соответствующих функций Win32 API.

Automation API предлагает еще множество функций для преобразования различных OLE-типов. Кто видел одну, видел все, но, в качестве бесплатного приложения, разберем аж две.

HRESULT VarI2FromI4(long lIn, short * psOut);
HRESULT VarI4FromI2(short sIn, long * plOut);

Первая функция преобразует long в short. Вторая функция, наоборот, преобразует short в long. Выглядит всё просто, но здесь есть проблемы с переполнением, которые COM хочет обрабатывать самостоятельно, вместо того, чтобы предоставить вам искать правильный путь. Некоторые функции преобразования, например BSTR в Boolean, или в дату из dispatch – несколько сложнее, так как имеют дополнительные параметры, но принципиально не отличаются. До недавнего времени они были недокументированы, и использовать их могли только посвященные. Теперь их описание входит в MSDN. Функция VariantChangeType внутренне использует многие из этих функций, но, если хотите, можете использовать их напрямую.

Пример:

short i2;
long i4;
hs = VarI2FromI4(i4, &i2);
if(DISP_E_OVERFLOW == hr)
  i2 = -1;
hs = VarI4FromI2(i2, &i4);
...

VARIANT и структуры

В этой статье есть раздел, посвященный структурам в Automation. Поэтому здесь мы не будем особо останавливаться на подробностях работы со структурами как таковыми, но о помещении их в Variant расскажем.

Для хранения структуры в Variant была добавлена структура, содержащая два поля:

struct __tagBRECORD        // VT_RECORD (структура)     ByRef Type
{
  PVOID pvRecord;          // Указатель на структуру
  IRecordInfo *pRecInfo;   // Описание структуры.
}   __VARIANT_NAME_4;

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

Поле pRecInfo должно при этом содержать указатель на интерфейс IRecordInfo, описывающий структуру указатель на которую помещен в pvRecord. Более подробно о создании и инициализации структур сказано в разделе, посвященном UDT.

Полю vt VARIANT-а, надо присвоить значение VT_RECORD. Это, собственно, и говорит окружающим, что в VARIANT содержится структура.

Важно понимать, что VARIANT никогда не хранит структуру как таковую, а только указатель на IRecordInfo. Например, если вызвать VariantClear для VARIANT типа VT_RECORD, будет вызван IRecordInfo::RecordClear для хранящейся структуры, что приведет к освобождению занимаемой ей памяти и всех данных, на которые ссылается структура. Однако, если вызвать VariantClear для VARIANT с VT_RECORD | VT_BYREF, будут очищены только поля.

Передача данных по ссылке (VT_BYREF)

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

С VT_BYREF связана еще одна проблема, которая зачастую проявляется при вызове методов dual-интерфейсов из скриптовых языков (WSH). Если такой метод содержит [in,out]-параметр, WSH поместит в список параметров метода Invoke интерфейса IDispatch VARIANT, содержащий ссылку (VT_BYREF) на реальную переменную, в которую метод должен поместить возвращаемые данные. 99% современных dual-интерфейсов не создаются вручную. Создается только их custom-часть, а disp-часть создается автоматически во время выполнения на основании описания из библиотеки типов. Для этого предназначены специальные API-функции: CreateStdDispatch, DispGetIDsOfNames, DispGetParam и DispInvoke. Проблема в том, что создаваемая таким образом реализация IDispatch не в состоянии правильно обработать [in,out]-параметр, переданный способом, описанным выше. Такая же проблема возникает с dual-интерфейсами, реализуемыми в VB 6.

К сожалению, для случая VB нормального решения этой проблемы нет. Единственное, что можно сделать – изменить описание самого метода так, чтобы вместо [in,out]-параметра конкретного типа использовался параметр типа VARIANT. При этом в параметр все равно будет помещена ссылка (VT_BYREF), но с ним уже будет разбираться не автоматическая реализация IDispatch, а ваш код.

Если объект создается на C++, например, с использованием билиотеки ATL, с этой проблемой можно справиться, изменив реализацию метода Invoke, находящуюся в шаблоне IDispatchImpl. Ниже приведен пример такой реализации.

IDL-описание метода с [in,out]-параметром:

[id(DISPID_COMMAND_EXECUTE),
  helpstring("Выполняет SQL-запрос в контексте данной команды."),
  helpcontext(IDH_ascDBRemoteLib_IascCommand_Execute)]
HRESULT Execute(
  [in, out] IascCachedCursor **Cursor,
  [in, defaultvalue("")] BSTR SQL,
  [in, defaultvalue(NULL)] IDispatch *Changer, 
  [in, defaultvalue(0)] long CursorID, 
  [in, defaultvalue(actDefault)] ASC_DB_CURSOR_TYPE DBCursorType, 
  [in, defaultvalue(aclDefault)] ASC_DB_CURSOR_LOCATION DBCursorLocation, 
  [in, defaultvalue(0)] long FetchBufferSize, 
  [in, optional] VARIANT UserData 
);

IascCachedCursor – это ссылка на объект, которая передается в и из метода.

Реализация шаблона, заменяющего реализацию IDispatch::Invoke, созданную с помощью CreateStdDispatch (для метода Execute):

// В скриптовых языках вместо указателя на IDispatch передается 
// (VT_VARIANT | VT_BYREF). Это приводит к неправильной работе метода
// (выдаче сообщения "TypeMismatch"). Данный класс заменяет IDispatchImpl<>
// и производит коррекцию первого параметра метода с DISPID,
// равным dispidExecute (задаваемым с помощью параметра шаблона).
template <DISPID dispidExecute, class T, const IID* piid, 
          const GUID* plibid = &CComModule::m_libid, WORD wMajor = 1,
          WORD wMinor = 0, class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE IDispatchImplSmartExec : 
  public IDispatchImpl<T, piid, plibid, wMajor, wMinor, tihclass>
{
public:
// IDispatch
  STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid,
    LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
    EXCEPINFO* pexcepinfo, UINT* puArgErr)
  {
    if(dispidExecute == dispidMember)
    {
      if(pdispparams); // Явно неправильная ситуация.
        return E_POINTER;
      if(pdispparams->cArgs < 1) // Явно неправильная ситуация.
        return E_INVALIDARG;
      
      if(pdispparams->rgvarg[0].vt == (VT_VARIANT | VT_BYREF))
      {
        // Кэшируем указатель на VARIANT, получаемый в нулевом параметре 
        // (имя параметра – Cursor), для упрощения дальнейшей работы.
        VARIANT * pVar = pdispparams->rgvarg[0].pvarVal;

        // Для начала нам нужно убедится, что переданный в качестве параметра
        // VARIANT содержит типы данных, с которыми мы способны работать...
        if(pVar->vt == VT_EMPTY || (pVar->vt == VT_DISPATCH && pVar->pdispVal))
        {
          // Указываем, что в нулевом параметре всегда передается 
          // VT_DISPATCH | VT_BYREF.
          pdispparams->rgvarg[0].vt = VT_DISPATCH | VT_BYREF;
          IDispatch * pIDispatch = NULL; // Указатель на случай VT_EMPTY

          // Если поле vt переданного нам по ссылке VARIANT содержит VT_EMPTY,
          // передаем в Execute указатель на временную переменную.
          // Иначе передаем двойной указатель на IDispatch 
          // прямо из варианта (pdispVal).
          pdispparams->rgvarg[0].ppdispVal = pVar->vt == VT_EMPTY 
                         ? &pIDispatch : &pVar->pdispVal;
          
          // Вызываем стандартную реализацию IDispatch для dual-интерфейсов,
          // как в IDispatchImpl<>.
          HRESULT hr = _tih.Invoke((IDispatch*)this, dispidMember, riid, lcid,
            wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
          
          // Теперь необходимо восстановить содержимое параметра.

          // Если возвращаемый объект был создан внутри метода Execute, 
          // сначала нужно поместить в исходный VARIANT указатель на 
          // полученный IDispatch (возвращаемого объекта).
          if(pVar->vt == VT_EMPTY && hr == S_OK)
          {
            pVar->vt = VT_DISPATCH;
            pVar->pdispVal = *pdispparams->rgvarg[0].ppdispVal;
          }

          // Теперь нужно восстановить значение первого параметра.
          pdispparams->rgvarg[0].pvarVal = pVar;
          pdispparams->rgvarg[0].vt = VT_VARIANT | VT_BYREF;
          return hr;
        }
      }
    }

    // В случае, если это не метод Execute, или что-то нас не устраивает,
    // передаем управление стандартной реализацию IDispatch для dual-
    // интерфейсов. Как в IDispatchImpl<>. Пущай разбирается.
    return _tih.Invoke((IDispatch*)this, dispidMember, riid, lcid,
      wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
  }
};

Применение шаблона в классе, реализующем интерфейс:

class ATL_NO_VTABLE CascCommand : 
  public CComObjectRootEx<CComMultiThreadModel>,
  public CComCoClass<CascCommand, &CLSID_ascCommand>,
  public CascPersistProperties<CascCommand>,
  public IascPersistStreamInitImpl<CascCommand>,
  public IascPersistPropertyBagImpl<CascCommand>,
  public ISupportErrorInfo,
  public IDispatchImplSmartExec<DISPID_COMMAND_EXECUTE, IascCommand, &IID_IascCommand, &LIBID_ascDBRemoteLib>
{
...

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

Структуры в Automation

В документации под псевдонимом UDT понимаются структуры. Так как мы с вами разумные люди, мы будем называть структуры структурами, даже несмотря на то, что их vt-тип называется записью (VT_RECORD).

COM практически с самого начала поддерживал работу со структурами. Но для маршалинга структур было необходимо создавать и компилировать proxy/stub. Использование структуры было возможно в основном из С и C++. Появившийся в середине 90-х Automation-маршалинг не поддерживал структур. Дело в том, что Automation-маршалинг подразумевает, что тип может быть передан через VARIANT (совместим с VARIANT), а во-вторых, что все описание интерфейса должно находиться в библиотеке типов.

В 1998 году, параллельно с выпуском Visual Studio 6, Microsoft выпустил Service Pack 4 для NT 4, который (с некоторыми ограничениями) позволил использовать структуры в Automation-совместимых интерфейсах. Впоследствии, после выхода W2k и SP6 для NT 4, технология была отлажена, и структурами стало можно пользоваться в серьезных проектах. В Windows 9х это заработает, если установить DCOM9х версии 1.3 или старше, если таковая появится. В Windows Me это будет работать и так, но под Windows 9х-Ме DCOM заведомо работает хуже.

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

Несмотря на ограничения и относительную сложность использования, в применении Automation-совместимых структур есть свой резон. Во-первых, если использовать только Automation-совместимые данные, исчезает необходимость компиляции proxy/stub DLL для клиента и сервера. Те, кому довелось помучиться с этим, оценят это достоинство. Во-вторых, Automation-совместимые интерфейсы дают большую совместимость. Например, такие структуры и их массивы будут понятны VB 6 и другим высокоуровневым языкам. К сожалению, из скриптовых языков доступа к полям структуры не будет, но можно написать нехитрую обертку, обеспечивающую доступ к полям. Сама же структура будет прекрасно храниться в переменной скриптового языка.

О том, как получить ссылку на интерфейс IRecordInfo, рассказывается в разделе, посвященном SAFEARRAY.

Перед использованием структуры (если она не используется совместно с SAFEARRAY), нужно создать экземпляр структуры и проинициализировать его. Для этого служат следующие функции:

PVOID IRecordInfo::RecordCreate();

Отводит память для нового экземпляра структуры, инициализирует этот экземпляр и возвращает указатель на него. Отведенная память при этом заполняется нулями, что соответствует VT_EMPTY. Для уничтожения созданных структур служит функция RecordDestroy.

RecordCreateCopy

Создает копию экземпляра структуры

HRESULT RecordDestroy([in] PVOID pvRecord);

Освобождает память, занятую экземпляром структуры и связанные с ней ресурсы.

HRESULT RecordInit(PVOID pvNew);

Инициализирует новый экземпляр структуры.

Остальные функции для работы со структурами не так интересны. Их описание можно найти в MSDN.

SAFEARRAY

Automation API предоставляет свой формат хранения массивов. Достоинство SAFEARRAY в том, что это универсальная структура и API, позволяющая хранить самоописываемые одномерные и многомерные массивы. «Самоописываемые» означает, что, имея ссылку на SAFEARRAY, можно получить полное описание хранящегося в нем массива. В это описание входят тип данных элемента, количество измерений и их верхние и нижние границы. Но SAFEARRAY создавались не для того, чтобы предоставлять информацию о себе, а для облегчения передачи массивов между компонентами компонентной среды.

SAFEARRAY – это единственный допустимый тип массива для dual-интерфейсов и интерфейсов, помеченных атрибутом oleautomation. При создании public-интерфейсов VB всегда использует SAFEARRAY и прекрасно их понимает сам.

Структура SAFEARRAY

Структура SAFEARRAY выглядит так:

typedef struct tagSAFEARRAY 
{
  USHORT cDims;
  USHORT fFeatures;
  USHORT cbElements;
  USHORT cLocks;
  USHORT handle;
  PVOID pvData;
  SAFEARRAYBOUND rgsabound[1];
}  SAFEARRAY;

Таблица 1

Флаги, используемые в поле fFeatures

Флаг

Описание

FADF_AUTO 0x0001

Массив, размещенный в стеке. Это используется в VB, если описать массив следующим образом:

Dim arry(0 To 2) As SomeType

Размер массива, размещенного в стеке, нельзя изменить.

FADF_STATIC 0x0002

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

FADF_EMBEDDED 0x0004

Массив, встроенный в структуру.

FADF_FIXEDSIZE 0x0010

Неперемещаемый массив, размер которого не может быть изменен.

FADF_RECORD 0x0020

Говорит о том, что элементом массива является структура. При этом в дескрипторе массива по отрицательному смещению 4 должен находиться указатель на IRecordInfo. Чтобы получить IRecordInfo можно воспользоваться функцией SafeArrayGetRecordInfo.

FADF_HAVEIID 0x0040

Массив, имеющий IID, идентифицирующий интерфейс. Говорит, что в дескрипторе массива по отрицательному смещению 16 (размер IID) должен находиться IID интерфейса являющегося элементом массива. Флаг может быть установлен только если задан флаг FADF_DISPATCH или FADF_UNKNOWN.

FADF_HAVEVARTYPE 0x0080

Массив типа VT. Говорит, что в дескрипторе массива по отрицательному смещению 4 должен присутствовать тег VT, указывающий тип элементов.

FADF_BSTR 0x0100

Массив элементов типа BSTR.

FADF_UNKNOWN 0x0200

Массив элементов типа IUnknown*.

FADF_DISPATCH 0x0400

Массив элементов типа IDispatch*.

FADF_VARIANT 0x0800

Массив элементов типа VARIANT.

FADF_RESERVED 0xF0E8

Зарезервировано.

Поле cbElements определяет размер элементов массива.

Поле cLocks – счетчик ссылок, указывающий количество блокировок, наложенных на массив.

Поле handle не используется. Оставлено для совместимости, и используется только на Mac'ах.

Поле pvData содержит указатель на данные массива.

Последнее поле rgsabound – массив, содержащий границы измерений (структур SAFEARRAYBOUND). В SAFEARRAY зарезервировано место только для одного элемента массива, описывающего измерения, но при создании многомерного массива API-функции автоматически выделяют память под все измерения. Структура SAFEARRAYBOUND выглядит следующим образом:

typedef struct tagSAFEARRAYBOUND
{
  ULONG cElements;
  LONG lLbound;
} SAFEARRAYBOUND;

Поле cElements содержит число элементов массива, а поле lLBound – нижняя граница.

Со структурой SAFEARRAY, в принципе, можно работать напрямую. Но делать этого не следует, так как это, во-первых, небезопасно, а во-вторых, через API-функции это делать удобнее.

Определение типа элемента

Определить тип элемента массива можно просто разобрав флаги в поле fFeatures. Но поскольку это поле не всегда реально содержит тип, а также ввиду сложности этого занятия, делать это имеет смысл только в отладочных целях. В коде проще и безопаснее пользоваться функцией SafeArrayGetVartype (см. далее).

Функции SAFEARRAY API

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

Пока речь идет о работе с одномерным массивом, функции SAFEARRAY просты и прямолинейны. С многомерными массивами дело хуже, отчасти потому, что у VB (для которого, собственно, и создавался SAFEARRAY) и VC++ разные идеи насчет размещения данных в разных измерениях. Кроме того, C++ хочет работать с массивами с отсчетом от нуля, а индекс типа SAFEARRAY может содержать отрицательные числа.

Далее будут даны аннотации к большинству этих функций. Краткие сведения обо всех функциях приведены в таблице 2.

Таблица 2

Функции, входящие в SAFEARRAY API.

Название функции

Описание

SafeArrayAccessData

Увеличивает счетчик блокировок массива и возвращает указатель на данные массива.

SafeArrayAllocData

Отводит память для SAFEARRAY на основании дескриптора, созданного SafeArrayAllocDescriptor.

SafeArrayAllocDescriptor

Отводит память под дескриптор SAFEARRAY.

SafeArrayAllocDescriptorEx

Отводит память под дескриптор SAFEARRAY для массива структур.

SafeArrayCopy

Копирует существующий массив.

SafeArrayCopyData

Копирует данные из одного массива в другой. Память под массив, куда копируются данные, не занимается и не перезанимается.

SafeArrayCreate

Создает новый SAFEARRAY. Позволяет создать многомерный SAFEARRAY.

SafeArrayCreateEx

Позволяет создать SAFEARRAY структур или указателей на интерфейсы.

SafeArrayCreateVector

Создает одномерный массив.

SafeArrayCreateVectorEx

Позволяет создать одномерный массив структур или указателей на интерфейсы.

SafeArrayDestroy

Уничтожает SAFEARRAY.

SafeArrayDestroyData

Освобождает память, занятую элементами данных SAFEARRAY-я.

SafeArrayDestroyDescriptor

Освобождает память, занятую дескриптором SAFEARRAY-я.

SafeArrayGetDim

Возвращает число измерений массива.

SafeArrayGetElemsize

Возвращает размер элемента.

SafeArrayGetIID

Возвращает IID элемента массива.

SafeArrayGetLBound

Находит нижнюю границу заданного измерения.

SafeArrayGetUBound

Находит верхнюю границу заданного измерения.

SafeArrayGetRecordInfo

Возвращает указатель на описание хранящейся в массиве структуры (IRecordInfo*).

SafeArrayGetVartype

Возвращает тип элемента массива.

SafeArrayLock

Блокирует массив и увеличивает его счетчик блокировок.

SafeArrayPtrOfIndex

Возвращает указатель на элемент массива.

SafeArrayPutElement

Помещает элемент в массив.

SafeArrayRedim

Изменяет размер массива.

SafeArraySetIID

Позволяет задать IID для элемента массива.

SafeArraySetRecordInfo

Позволяет задать IRecordInfo.

SafeArrayUnaccessData

Освобождает указатель на данные массива и уменьшает счетчик блокировок массива.

SafeArrayUnlock

Уменьшает счетчик блокировок массива.

HRESULT SafeArrayGetVartype(SAFEARRAY * psa,
            VARTYPE * pvt);

Эта функция принимает дескриптор массива, и, если это возможно, возвращает в переменную, на которую ссылается параметр pvt, тип элемента массива. Если в массиве находится простой тип, все просто. Но в случае, когда в массиве содержатся структуры, данная функция вернет VT_RECORD. Чтобы определить, что за структура хранится в массиве, нужно воспользоваться функцией SafeArrayGetRecordInfo. Если в массиве хранится указатель на интерфейс, и для этого интерфейса задан IID, этот IID можно получить с помощью функции SafeArrayGetIID. Это довольно редко случается, поэтому лучше знать какой интерфейс лежит в массиве. В крайнем случае, можно попытаться получить информацию об интерфейсе у самого интерфейса. Это довольно просто сделать, если объект поддерживает IDispatch.

HRESULT SafeArrayGetRecordInfo(SAFEARRAY * psa,
IRecordInfo ** prinfo);

В случае если в массиве хранятся структуры, возвращает указатель на интерфейс IRecordInfo. С помощью IRecordInfo можно определить тип структуры и получить ее описание.

HRESULT SafeArrayGetIID(SAFEARRAY * psa, GUID * pguid);

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

SAFEARRAY * SafeArrayCreate( VARTYPE vt, 
        UINT cDims, SAFEARRAYBOUND * rgsabound);
SAFEARRAY * SafeArrayCreateVector(VARTYPE vt, 
        LONG lLbound, ULONG cElements);
HRESULT SafeArrayDestroy(SAFEARRAY * psa);

Одномерные массивы удобно создавать с помощью функции SafeArrayCreateVector. В первом параметре ей передается тип элемента массива, во-втором (lLbound) – нижняя граница массива, а в третьем – количество элементов в массиве.

SafeArrayCreate позволяет создать массив с любым количеством измерений, но из-за сложности ее лучше использовать, только если необходимо создать многомерный массив. Как и предыдущая функция, она принимает тип массива в первом параметре vt. Во втором параметре (cDims) функция получает число измерений, а в третьем параметре (adims) – описания измерений (массив структур SAFEARRAYBOUND). Функции SafeArrayCreate и SafeArrayCreateVector создают новый массив, отводят место под его данные и возвращают указатель на структуру SAFEARRAY. Когда надобность в массиве отпадет, необходимо вызвать SafeArrayDestroy. SAFEARRAY могут работать с любыми VARIANT-типами, но VT_ARRAY, VT_BYREF, VT_EMPTY и VT_NULL не поддерживаются.

При уничтожении массива (с помощью SafeArrayDestroy), тип элемента которого BSTR, VARIANT или указатель на интерфейс, для каждого элемента вызывается SysFreeString, VariantClear и Release соответственно.

Пример:

// Создаем новый 1-D массив Integer-ов.
SAFEARRAY * psaiNew; 
// Эквивалентно: Dim aiNew(1 To 8) As Integer.
psaiNew = SafeArrayCreateVector(VT_I2, 1, 8);
if(psaiNew != NULL)
{
  // Используем массив.
  ...
  SafeArrayDestroy(psaiNew);
}
SafeArrayCreateVectorEx и SafeArrayCreateEx

Это более новые варианты функций SafeArrayCreateVector и SafeArrayCreate, соответственно. Они отличаются от своих предшественников только одним параметром pvExtra. Этот параметр позволяет задать информацию о структуре (IRecordInfo), если тип элемента массива (vt) VT_RECORD, и IID, если тип – VT_DISPATCH или VT_UNKNOWN. Во втором случае можно обойтись и без задания IID.

HRESULT SafeArrayAllocDescriptor(UINT cDims, 
          SAFEARRAY ** ppsaOut);
HRESULT SafeArrayAllocData(SAFEARRAY * psa);
HRESULT SafeArrayDestroyData(SAFEARRAY * psa);
HRESULT SafeArrayDestroyDescriptor(SAFEARRAY * psa);

Эти функции – более сложная и гибкая замена SafeArrayCreate и SafeArrayDestroy. В массив можно помещать несовместимые с Automation типы данных, но управлять данными придется самому. На практике эти функции используются нечасто. Если вам позарез нужны именно эти функции, обратитесь к MSDN.

HRESULT SafeArrayGetElement(SAFEARRAY * psa, 
          long * aiIndex, void * pvElem);
HRESULT SafeArrayPutElement(SAFEARRAY * psa, 
          long * aiIndex, void * pvElem);

Эти функции извлекают из массива или вставляют в массив один элемент. Одной из них передается указатель на массив и массив индексов для нужного элемента. Функция возвращает указатель на элемент через параметр pvElem. Нужно знать число измерений массива и предоставить массив индексов нужного размера. Самое правое (наименее существенное) измерение должно быть aiIndex[0], а самое левое – aiIndex[psa->cDims-1]. До и после обращения к элементу эти функции автоматически вызывают SafeArrayLock и SafeArrayUnlock. Если элемент данных – это BSTR, VARIANT или объект, он корректно копируется, а в случае указателя на интерфейс увеличивается счетчик ссылок. Возможны множественные блокировки массива, так что можно использовать эти функции, когда массив блокирован другими операциями.

Эти функции с виду просты, но порождают довольно сложный и плохо читаемый код. Вдобавок они довольно медленно работают. На C++ более эффективно использовать функцию SafeArrayAccessData, о которой будет рассказано ниже.

Ниже приведен пример, в котором значение каждой ячейки SAFEARRAY (с типом элемента VT_I4) извлекается, увеличивается на единицу и помещается обратно:

long iVal;
long iLBound;
long iUBound;

hr = SafeArrayGetLBound(psaiInOut, 0, &iLBound);
  if(FAILED(hr))
    return hr;
hr = SafeArrayGetUBound(psaiInOut, 0, &iUBound);
  if(FAILED(hr))
    return hr;
for (i = 0; i <= iUBound - iLBound; i++)
{
  hr = SafeArrayGetElement(psaiInOut, i, &iVal);
  if(FAILED(hr))
    return hr;

  iVal++; // Увеличиваем значение ячейки массива.

  hr = SafeArrayPutElement(psaiInOut, i, &iVal);
  if(FAILED(hr))
    return hr;
}
HRESULT SafeArrayLock(SAFEARRAY * psa);
HRESULT SafeArrayUnlock(SAFEARRAY * psa);

Эти функции уменьшают и увеличивают счетчик блокировок массива. Данные становятся доступными через поле pvData дескриптора массива. Указатель в дескрипторе массива действителен, пока не вызвана функция SafeArrayUnlock. Заметьте, что поле pvData, как все массивы C++, индексировано с нуля. Если нижняя граница массива отлична от нуля, придется учитывать это вручную.

Эти функции являются замечательным примером попытки угодить всем подряд, даже тем, кто наотрез не согласен с принципами безопасного программирования. Лучше не пользоваться этими изумительными функциями, а использовать SafeArrayAccessData и SafeArrayUnaccessData.

HRESULT SafeArrayPtrOfIndex(SAFEARRAY * psa, 
           long * aiIndex, void ** ppv);

Эта функция возвращает указатель на элемент массива. Ей нужно передать массив значений индексов, идентифицирующий элемент. Перед вызовом SafeArrayPtrOfIndex массив нужно блокировать (SafeArrayLock).

Пример:

xMin = aDims[0].lLbound; 
xMax = xMin + (int)aDims[0].cElements - 1;
yMin = aDims[1].lLbound; 
yMax = yMin + (int)aDims[1].cElements - 1;
int * piInOut;
if(hr = SafeArrayLock(psaiInOut)) // Блокируем и изменяем 2-D массив.
  return hr;
for(x = xMin; x <= xMax; x++)
{
  ai[0] = x;
  for(y = yMin; y <= yMax; y++)
  {
    ai[1] = y;
    hr = SafeArrayPtrOfIndex(psaiInOut, ai, (void **)&piInOut);
    if(hr)
      return hr;
    (*piInOut)++; // Увеличиваем значение ячейки массива.
  }
}
SafeArrayUnlock(psaiInOut);
HRESULT SafeArrayAccessData (SAFEARRAY * psa, 
             void ** ppvData);
HRESULT SafeArrayUnaccessData(SAFEARRAY * psa);

Функции SafeArrayAccessData передается указатель на SAFEARRAY и переменная для получения адреса данных массива; она блокирует массив и возвращает указатель на данные. Когда все сделано, вызывается SafeArrayUnaccessData. Эта функция позволяет получать доступ к данным массива за один прием. При этом достаточно привести указатель на данные к указателю на элемент массива и можно работать, как будто массив является обычным С-массивом. Ниже приведен пример, сортирующий одномерный массив BSTR-ов методом пузырьковой сортировки.

long i, j, min; 
BSTR bstrTemp;
BSTR HUGEP *pbstr;
HRESULT hr;
// Получить указатель на данные.
hr = SafeArrayAccessData(psa, (void HUGEP**)&pbstr);
if(FAILED(hr))
  return hr;
// Сортируем методом выбора.
for (i = 0; i < psa->rgsabound.cElements-1; i++)
{
   min = i;
   for (j = i+1; j < psa->rgsabound.cElements; j++)
   {
      if (wcscmp(pbstr[j], pbstr[min]) < 0)
         min = j; 
   }

   // Меняем элементы массива array[min] и array[i] местами.
   bstrTemp = pbstr[min];
   pbstr[min] = pbstr[i];
   pbstr[i] = bstrTemp;

}

SafeArrayUnaccessData(psa);
HRESULT SafeArrayCopy(SAFEARRAY * psaIn, 
              SAFEARRAY ** ppsaOut);

Эта функция создает копию существующего массива. Ей передается указатель на копируемый массив и адрес указателя на SAFEARRAY, получающий копию. Если исходный массив содержит типы BSTR или VARIANT, SafeArrayCopy вызывает для создания копии соответствующие системные функции. Если же исходный массив содержит ссылки на объекты, SafeArrayCopy увеличивает их счетчики ссылок. В конце концов получаются два идентичных массива. Пример:

SAFEARRAY * psaiCopy; 
hr = SafeArrayCopy(psaiFirst, &psaiCopy);
UINT SafeArrayGetDim(SAFEARRAY * psa);
UINT SafeArrayGetElemsize(SAFEARRAY * psa);

Эти функции возвращают число измерений массива и размер элемента в байтах соответственно. Они эквивалентны получению соответствующих элементов из дескриптора. Пример:

int cDim = (int)SafeArrayGetDim(psaiInOut);
int cbElem = (int)SafeArrayGetElemsize(psaiInOut); 
HRESULT SafeArrayGetLBound(SAFEARRAY * psa, 
              UINT cDim, long * piLo);
HRESULT SafeArrayGetUBound(SAFEARRAY * psa, 
              UINT cDim, long * piUp);

Эти функции возвращают нижнюю и верхнюю границу заданного измерения массива. Пример:

SAFEARRAYBOUND * aDims = new SAFEARRAYBOUND[cDim];
long iT;
for (i = 0; i < cDim; i++)
{
  hr = SafeArrayGetLBound(psaiInOut, i + 1, &aDims[i].lLbound);
  if(hr)
    return hr;
  hr = SafeArrayGetUBound(psaiInOut, i + 1, &iT);
  if(hr)
    return hr;

  // Подсчитываем количество элементов.
  aDims[i].cElements = iT - aDims[i].lLbound + 1;
}
HRESULT SafeArrayRedim(SAFEARRAY * psa, 
                SAFEARRAYBOUND * pdimNew);

Эта функция позволяет изменить количество элементов в массиве. SafeArrayRedim передается указатель на массив и указатель на переменную SAFEARRAYBOUND, содержащую нужные измерения. Если вы уменьшаете размер массива, SafeArrayRedim перемещает элементы массива за новую границу массива. При увеличении размера SafeArrayRedim размещает и инициализирует новые элементы массива. Данные сохраняются только в элементах, существующих и в старом, и в новом массиве. Чтобы изменить размер массива, переданного из VB, массив должен быть non-static массивом:

' Такое определение массива позволяет использовать SafeArrayRedim.
Dim aiModify () As Integer
ReDim Preserve aiModify(1 To 8, 1 To 8) As Integer
' А такое - нет.
Dim aiFixed(1 To 8, 1 To 8) As Integer

Идентифицировать массив фиксированной длины, переданный из VB, можно по полю fFeatures структуры SAFEARRAY. Массивы VB с фиксированной длиной, объявленные с помощью Dim, Private или Public, имеют флаги FADF_STATIC и FADF_FIXEDSIZE. Массивы, размер которых задан с помощью VB-выражения ReDim (и потому работающие с SafeArrayRedim) таких флагов не имеют. Пример:

// Удвоим количество элементов последнего измерения.
i = cDim - 1;
aDims[i].cElements *= 2;
if (hres = SafeArrayRedim(psaiInOut, &aDims[i])) throw hres;

Структуры в SAFEARRAY

Для работы с automation-структурами необходимо иметь указатель на интерфейс IRecordInfo, описывающий структуру. Если имеется ссылка на SAFEARRAY, этот интерфейс можно получить с помощью метода SafeArrayGetRecordInfo. IRecordInfo можно так же позаимствовать из VARIANT, если в нем находится структура требуемого типа. Если же получить IRecordInfo вышеперечисленными способами невозможно, придется получать его с помощью функции GetRecordInfoFromGuids или GetRecordInfoFromTypeInfo. Это значительно сложнее, но все же доступно большинству смертных. :) Чуть позже вы увидите, как они работают.

Чтобы эти функции работали, естественно, необходимо, чтобы описание структуры присутствовало в библиотеке типов. В языках типа VB достаточно описать структуру (в VB 6 они называются TYPE) в public-модуле (DLL или EXE COM-сервера) и пометить её как public. С/C++-программисту придется определенным образом описывать структуры в IDL-файле. «Определенным образом» означает, что при описании структуры в IDL необходимо задать GUID этой структуры. Это делается с помощью атрибута uuid. Вот пример, описывающий структуру и метод:

import "oaidl.idl";
import "ocidl.idl";

[
  uuid(2CC7E043-47FA-4798-A086-D0AC6A667320),
  version(1.0),
  helpstring("RecInf1 1.0 Type Library")
] library RecInf1Lib
{

  importlib("stdole2.tlb");

  typedef 
    enum tagclassification 
    { 
      school_Bully, class_Clown, teachers_Favorite
    } 
    classification;

  [uuid(D8B3861A-74C6-11d2-0000-00C04FB17CDB)] 
  struct StudentStruct
  {
      BSTR name;
      short grade;
      classification  type;
      VARIANT_BOOL   graduate;
  };

  [
    object,
    uuid(D50BD660-7471-11d2-9A80-006097DA88F5),  // IID_IStudent
    helpstring("User Defined Data Server"),
    dual,
    pointer_default(unique)
  ]
  interface IStudent : IDispatch
  {
    import "unknwn.idl";
    typedef IStudent* LPSTUDENT;
  
    HRESULT Test1([in,out] SAFEARRAY(struct StudentStruct) * student);
  };
  [
    uuid(7DC46F8D-E0D9-4604-B551-89F14E797BAF),
    helpstring("Test Class")
  ]
  coclass Test
  {
     [default] interface IStudent;
  };
};

Для совместимости с C в документации, примерах, да и на практике традиционно принято использовать typedef при описании структур. Дело в том, что язык C обязывает программиста при указании пользовательского (не встроенного) типа указывать модификатор этого типа. Например, для структуры StudentStruct без использования typedef приходится писать «struct StudentStruct» (что и сделано в приведенном выше примере). Вопреки расхожему мнению, синтаксис IDL в данном случае взят не из C++, а из С. Поэтому в IDL при указании структуры также необходимо добавлять ключевое слово struct. В IDL, как и в С, typedef является легальным способом обхода этого неудобства и позволяет избежать использования этого ключевого слова. Но в IDL, в отличие от C/C++, typedef это довольно сложная конструкция, которая может иметь свои атрибуты. В список атрибутов входит uuid и public. Атрибут public превращает typedef из локальной подстановки в полноценный тип (alias) доступный из языков высокого уровня вроде (VB). Если описать структуру через typedef с указанием атрибута public (или inline, т.е. описанием структуры внутри typedef-а), то MIDL не присваивает GUID структуре. Если раздельно описать структуру с GUID и typedef (тоже с GUID, но с другим):

[uuid(0FC75F57-4107-4fca-0000-FADF03C8F52B)] 
struct StudentStruct 
{
  BSTR name;
  short grade;
  classification  type;
  VARIANT_BOOL   graduate;
};

typedef [uuid(0FC75F57-4107-4fca-AAAA-FADF03C8F52B), public] 
  struct StudentStruct StudentStruct;

то при попытке использовать результат typedef в описании метода будет выдано следующее сообщение об ошибке:

error MIDL2270 : duplicate UUID. Same as : 
StudentStruct [ Type 'StudentStruct' ( Parameter 'student' ) ]

То есть присвоить GUID структуре и одновременно typedef-у этой структуры нельзя

В принципе, достаточно оставить GUID только для typedef (это достижимо если описывать структуру в inline-стиле). При этом описание структуры свободно можно будет получить функциями GetRecordInfoFromGuids и GetRecordInfoFromTypeInfo, но появится одна проблема.

К сожалению, в MIDL есть маленькая, но очень противная ошибка, связанная с неправильной генерацией (MIDL) кода при объявлении структур через typedef . Причем есть два варианта ошибки. Если typedef добавлен отдельным оператором (после объявления структуры):

typedef struct tagStudentStruct StudentStruct;

то MIDL не ассоциирует GUID со структурой. И хотя VB и умудряется каким-то образом видеть такие описания (видимо, работая с элементами библиотеки типов по именам), получить IRecordInfo для такой структуры не выйдет. В библиотеке типов с описанием структуры попросту не будет ассоциирован GUID, а имеющиеся API-функции рассчитаны на его наличие.

В принципе, ничего страшного том, чтобы действовать так, как указано в нашем примере (то есть в обязательном указании ключевого слова struct), нет, но программисту, привыкшему к синтаксису C++, такая запись будет раздражать глаз. Поэтому использование typedef стало стандартом де-факто. В MSDN, откуда изначально был взят приведенный выше пример, использовался typedef (причем в inline-стиле). Однако в приведенном здесь примере typedef не используется, и где нужно подставляется ключевое слово struct. Ниже приведен пример, взятый из MSDN:

typedef [uuid(D8B3861A-74C6-11d2-A0D6-00C04FB17CDB)] struct tagStudentStruct
{
  BSTR name;
  short grade;
  classification  type;
  VARIANT_BOOL   graduate;
} StudentStruct;

При компиляции этого кода возникает предупреждение от компилятора MIDL, которое довольно легко пропустить:

warning MIDL2368 : error generating type library, ignored : Could not set UUID: tagStudentStruct (0x800288C6)

Это предупреждение говорит о том, что MIDL не стал ассоциировать GUID со структурой. Вместо этого MIDL ассоциировал GUID с typedef-ом. Однако, раз он решил не ассоциировать GUID со структорой в библиотеке типов, он и не будет делать этого при генерации заголовочных C++-файлов.

Вот ошибочный код, сгенерированный MIDL с использованием кода от Microsoft.

// Заголовочный файл C++, сгенерированный MIDL
typedef /* [uuid] */ DECLSPEC_UUID("D8B3861A-74C6-11d2-A0D6-00C04FB17CDB") 
struct tagStudentStruct
{
  BSTR name;
  short grade;
  classification type;
  VARIANT_BOOL graduate;
} StudentStruct;

А вот код, созданный тем же компилятором с использованием кода из нашего примера.

/* [uuid] */ struct DECLSPEC_UUID("D8B3861A-74C6-11d2-A0D6-00C04FB17CDB")
StudentStruct
{
  BSTR name;
  short grade;
  classification type;
  VARIANT_BOOL graduate;
} ;

Обратите внимание на выделенный текст. Получается, что в первом случае DECLSPEC_UUID ассоциируется с typedef, а не со стуктурой. При этом получается, что GUID повисает в воздухе, так как получить его от типа, сделанного с помощью typedef, невозможно. В общем, факт в том, что компилятор попросту выкидывает ассоциацию GUID-а (DECLSPEC_UUID). Если библиотеку типов создаете вы, лучше обойтись без typdef (как в коде нашего примера), при этом можно будет просто написать нечто типа:

GUID GUID_student = __uuidof(StudentStruct);

Если заголовочный файл создан не вами, и вы не хотите его исправлять, можно написать повторную ассоциацию GUID со структурой, например, так:

struct DECLSPEC_UUID("D8B3861A-74C6-11d2-A0D6-00C04FB17CDB") StudentStruct;
// теперь можно использовать __uuidof
GUID GUID_student = __uuidof(StudentStruct);

Однако мы отвлеклись.

К сожалению, GUID недостаточно, чтобы получить IRecordInfo. Функции GetRecordInfoFromGuids нужно передать еще и информацию о библиотеке типов, в которой описана эта структура. Библиотека типов описывается кучей параметров, и делать это крайне неудобно, но тут ничего не поделаешь. Самое обидное, что при переходе на новую версию библиотеки придется заменять все эти значения, так что лучше сразу задать информацию о библиотеке типов в виде констант, поместив их в общий заголовочный файл. Туда же можно (при необходимости) поместить и ассоциацию GUID со структурой.

Еще одним выходом может стать описание GUID в виде обычной константы, но делать это крайне неудобно, так как в IDL-файле GUID объявляется в строковом виде («CE1CDD94-C7BF-48a3-B29F-AB210D310AC2»), а при определении констант необходимо будет перевести его в нечто вроде следующего:

DEFINE_GUID(<<name>>, 
  0xce1cdd94, 0xc7bf, 0x48a3, 0xb2, 0x9f, 0xab, 0x21, 0xd, 0x31, 0xa, 0xc2);

или этого:

DEFINE_GUID(<<name>>, 
  0xce1cdd94, 0xc7bf, 0x48a3, 0xb2, 0x9f, 0xab, 0x21, 0xd, 0x31, 0xa, 0xc2);

В C++ тип элемента SAFEARRAY можно определить только во время исполнения. Но языки типа VB 6 эмулируют работу с SAFEARRAY как с типизированным массивом. Для совместимости с VB в библиотеки типов была введена возможность указывать, что в параметре передается SAFEARRAY с элементом определенного типа. Параметр student метода Test1 в отрывке IDL, приведенном выше, объявлен как SAFEARRAY с типом элемента StudentStruct.

Если нужно создать новый экземпляр SAFEARRAY-я структур, следует использовать функцию SafeArrayCreateEx или SafeArrayCreateVectorEx, последним параметром которым необходимо передать указатель на IRecordInfo.

Для демонстрации работы с SAFEARRAY структур мы создали тестовый COM-объект «Test» (CTect) реализующий описанный выше интерфейс. Реализация сделана на ATL. Ниже приводятся ее фрагменты.

Итак, чтобы использовать automation-структуры, сперва необходимо получить ее описание. А для этого нужен GUID структуры и LibID библиотеки типов. Если у вас есть заголовочные файлы, сгенерированные MIDL, всю эту информацию можно взять оттуда. Если же вы пишете клиентское приложение, и заголовочные файлы вам недоступны, можно воспользоваться директивой #import:

#import "c:\MyProjects\Hren\RecInf1\Debug\RecInf1.dll" no_namespace, \
   named_guids, raw_interfaces_only, no_implementation, no_smart_pointers, \
   raw_dispinterfaces

К сожалению, это нестандартное расширение C++ от Microsoft. В компиляторах от Borland тоже есть такая опция, но она практически не работает. Зато в C++ Builder есть возможность генерировать обертки собственного (Delphi-йского) формата. У компиляторов других производителей обычно вообще нет никакой поддержки COM. Так что если вы не используете VC, желательно иметь заголовочные файлы, сгенерированные MIDL.

Следующий код загружает информацию о структуре из библиотеки типов:

GUID GUID_StudentStruct = __uuidof(StudentStruct);
HRESULT hr;

// Получаем IRecordInfo из библиотеки типов.
CComPtr<IRecordInfo> spRecInfo;
hr = GetRecordInfoFromGuids(LIBID_RecInf1Lib, 1, 0, 0,
       GUID_StudentStruct, &spRecInfo);
if(FAILED(hr))
  _com_issue_errorex(hr, NULL, IID_NULL);

В случае успеха вы будете обладать указателем на IRecordInfo нашей структуры. Если что-то пойдет не так, вы получите сообщение об ошибке. Если вы получили сообщение об ошибке, и из его кода непонятно, что происходит, то функцию GetRecordInfoFromGuids можно заменить на более объемистый код с использованием GetRecordInfoFromTypeInfo. В этом случае придется вручную загрузить библиотеку типов (с помощью LoadRegTypeLib), найти описание (ITypeInfo) этой структуры (с помощью GetTypeInfoOfGuid) и попытаться получить описание структуры (в данном случае указатель на IRecordInfo) уже от него. Если проблемы связаны с загрузкой библиотеки типов, вы это поймете. Если же нет – смотрите описание этой библиотеки и пытайтесь понять, в чем может быть проблема (чаще всего встречается проблема с typedef, описанная выше).

Чтобы создать новый массив, нужно воспользоваться SafeArrayCreateEx или SafeArrayCreateVectorEx:

const iLBound = 0, iUBound = 2;
// Создаем SAFEARRAY со структурой в качестве элемента StudentStruct
LPSAFEARRAY psaStudent = SafeArrayCreateVectorEx(VT_RECORD, iLBound,
  iUBound - iLBound + 1, spRecInfo);
if(!psaStudent)
  _com_issue_errorex(E_FAIL, NULL, IID_NULL);

Если массив уже имеется (создан или получен извне), можно получить доступ к его элементам как к обычному C++-массиву структур...

// Получаем доступ к данным этого массива.
StudentStruct * pStudentStruct = NULL;
hr = SafeArrayAccessData(psaStudent, (void**)&pStudentStruct);
if(FAILED(hr))
  _com_issue_errorex(hr, NULL, IID_NULL);

затем можно модифицировать данные в ячейках...

//  Заполняем массив данными.
USES_CONVERSION;
TCHAR szBuff[100];
for(int i = 0, i2 = iLBound; i <= iUBound - iLBound; i++, i2++)
{
  wsprintf(szBuff, _T("Элемент %d"), i2);
  pStudentStruct[i].name = SysAllocString(T2OLE(szBuff));
}

и, естественно, не забыть «отпустить" массив.

// "Отпускаем" массив.
hr = SafeArrayUnaccessData(psaStudent);
if(FAILED(hr))
  _com_issue_errorex(hr, NULL, IID_NULL);

Массив создан, данные в него заложены, теперь можно передать его в вызываемый метод или засунуть в VARIANT. Ниже приведен код метода нашего тестового объекта:

// Создаем тестовый COM-объект.
CComPtr<IStudent> spIStudent;
hr = spIStudent.CoCreateInstance(CLSID_Test);
if(FAILED(hr))
  _com_issue_errorex(hr, NULL, IID_NULL);

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

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

HRESULT TraceSafeArray(SAFEARRAY * psa)
{
  // Считываем данные из возвращенного массива.
  USES_CONVERSION;
  LONG iLBound, iUBound;
  HRESULT hr = SafeArrayGetLBound(psa, 1, &iLBound);
  if(FAILED(hr))
    return hr;
  hr = SafeArrayGetUBound(psa, 1, &iUBound);
  if(FAILED(hr))
    return hr;

  StudentStruct * pStudentStruct = NULL;
  hr = SafeArrayAccessData(psa, (void**)&pStudentStruct);
  if(FAILED(hr))
    return hr;

  for(int i = 0; i <= iUBound - iLBound; i++)
    ATLTRACE("%s\n", OLE2T(pStudentStruct[i].name));

  hr = SafeArrayUnaccessData(psa);
  if(FAILED(hr))
    return hr;
  return S_OK;
}

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

Следующий код вызывает эту функцию до и после вызова метода Test1:

ATLTRACE("Содержание массива до вызова метода.\n");
TraceSafeArray(psaStudent);

// Вызываем тестовый объект и передаем ему созданный нами SAFEARRAY.
ATLTRACE("Вызван метод Test::Test1(SAFEARRAY ** student)\n");
hr = spIStudent->Test1(&psaStudent);
if(FAILED(hr))
  _com_issue_errorex(hr, spIStudent, __uuidof(spIStudent));
ATLTRACE("Выход из метода Test::Test1(SAFEARRAY ** student)\n");

ATLTRACE("Содержание массива после вызова метода. \n");
TraceSafeArray(psaStudent);

Для полной ясности приведу код метода Test1:

STDMETHODIMP CTest::Test1(SAFEARRAY ** student)
{
  // Следующую строку можно применять, если не удается получить GUID 
  // от структуры (GUID не ассоциирован со структурой), но лучше все же 
  // просто описывать структуры без применения typedef.
  //struct DECLSPEC_UUID("D8B3861A-74C6-11d2-A0D6-00C04FB17CDB")
  //            StudentStruct;
  GUID GUID_StudentStruct = __uuidof(StudentStruct);
  HRESULT hr;

  ATLTRACE("Содержание присланного массива.\n");
  TraceSafeArray(*student);

  CComPtr<IRecordInfo> spRecInfo;
  hr = GetRecordInfoFromGuids(LIBID_RecInf1Lib, 1, 0, 0, 
    GUID_StudentStruct, &spRecInfo);
  if(FAILED(hr))
    return hr;
  const iLBound = 2, iUBound = 10;
  LPSAFEARRAY psaStudent = SafeArrayCreateVectorEx(VT_RECORD, iLBound, 
    iUBound - iLBound + 1, spRecInfo);
  if(!psaStudent)
    return E_FAIL;
  StudentStruct * pStudentStruct = NULL;
  hr = SafeArrayAccessData(psaStudent, (void**)&pStudentStruct);
  if(FAILED(hr))
    return hr;

  USES_CONVERSION;
  TCHAR szBuff[100];
  for(int i = 0, i2 = iLBound; i <= iUBound - iLBound; i++, i2++)
  {
    wsprintf(szBuff, _T("Имя %d"), i2);
    // Работаем со структурой, как будто она родная, C++-ая.
    pStudentStruct[i].name = SysAllocString(T2OLE(szBuff));
  }
  hr = SafeArrayUnaccessData(psaStudent);
  if(FAILED(hr))
    return hr;

  SafeArrayDestroy(*student);
  *student = psaStudent;
  ATLTRACE("Содержание возвращаемого массива. \n");
  TraceSafeArray(*student);
  return S_OK;
}

Если запустить пример, то в отладочном окне VS появится следующий вывод:

Содержание массива до вызова метода.
Элемент 0
Элемент 1
Элемент 2
Вызван метод Test::Test1(SAFEARRAY ** student)
Содержание присланного массива.
Элемент 0
Элемент 1
Элемент 2
Содержание возвдащаемого массива.
Имя 2
Имя 3
Имя 4
Имя 5
Имя 6
Имя 7
Имя 8
Имя 9
Имя 10
Выход из метода Test::Test1(SAFEARRAY ** student)
Содержание массива после вызова метода.
Имя 2
Имя 3
Имя 4
Имя 5
Имя 6
Имя 7
Имя 8
Имя 9
Имя 10

В конце важно не забыть уничтожить SAFEARRAY, чтобы не было утечки памяти.

Проверить наш пример можно и из VB 6 (или VBA) вот простенький тест:

Sub test()
    Dim student() As RecInf1Lib.StudentStruct
    ReDim student(0 To 2)
    Dim t As New RecInf1Lib.test
    student(0).Name = "Âàñÿ"
    student(1).Name = "Êàòÿ"
    student(2).Name = "Ïåòÿ"
    t.Test1 student
End Sub

Главное – не забыть подключить библиотеку типов («RecInf1 1.0 Type Library»). Поставьте точку останова на последней строке и посмотрите на результаты работы метода в отладчике.


Copyright © 1994-2016 ООО "К-Пресс"