Типы данных
Все интерфейсы СОМ должны быть определены в IDL. IDL позволяет описывать довольно сложные типы данных в стиле, не зависящем от языка и платформы. Рисунок 2.6 показывает базовые типы, которые поддерживаются IDL, и их отображения в языки С, Java и Visual Basic. Целые и вещественные типы не требуют объяснений. Первые "интересные" типы данных, встречающиеся при программировании в СОМ, — это символы и строки.
Основные типы | boolean | unsigned char | unsupported | char |
byte | unsigned char | unsupported | char | |
small | char | unsupported | char | |
short | short | Integer | short | |
long | long | Long | int | |
hyper | _int64 | unsupported | long | |
float | float | Single | float | |
double | double | Double | double | |
char | unsigned char | unsupported | char | |
wchar_t | wchar_t | Integer | short | |
enum | enum | Enum | int | |
Interface Pointer | Interface Pointer | Interface Ref. | Interface Ref. | |
Расширенные типы | VARIANT | VARIANT | Variant | ms.com.Variant |
BSTR | BSTR | String | java.lang.String | |
VARIANT_BOOL | short [-1/0] | Boolean [True/False] | boolean [true/false] |
Рис. 2.6. Базовые типы СОМ
Все символы в СОМ представлены с использованием типа данных OLECHAR. Для Windows NT, Windows 95, Win32s и Solaris OLECHAR — это просто typedef для типа данных С wchar_t. Специфика других платформ описана в соответствующих документациях. Платформы Win32 используют тип данных wchar_t для представления 16-битных символов Unicode. Поскольку типы указателей в IDL созданы так, что указывают на одиночные переменные, а не на массивы, то IDL вводит атрибут [string], чтобы подчеркнуть, что указатель указывает на массив-строку с завершающим нулем:
HRESULT Method([in, string] const OLECHAR *pwsz);
Для определения строк и символов, совместимых с OLECHAR, в СОМ введен макрос OLESTR, который приписывает букву L перед строковой или символьной константой, информируя таким образом компилятор о том, что эта константа имеет тип wchar_t. Например, правильным будет такой способ инициализировать указатель OLECHAR с помощью строкового литерала:
const OLECHAR *pwsz = OLESTR("Hello");
Под Win32 или Solaris это эквивалентно
const wchar_t *pwsz = L"Hello";
Первый вариант предпочтительней, так как он будет надежно компилироваться на всех платформах.
Поскольку часто возникает необходимость копировать строки на основе типа wchar_t в обычные буфера на основе char, то динамическая библиотека С предлагает две процедуры для преобразования типов:
size_t mbstowcs(wchar_t *pwsz, const char *psz, int cch); size_t wcstombs(char *psz, const wchar_t *pwsz, int cch);
Эти две процедуры работают аналогично динамической С-процедуре strncpy, за исключением того, что в эти процедуры как часть операции копирования включено расширение или сужение строки. Следующий код показывает, как параметр метода, размещенный в OLECHAR, можно скопировать в элемент данных, размещенный в char:
class BigDog : public ILabrador { char m_szName[1024] ; public: STDMETHODIMP SetName(/* [in,string]*/ const OLECHAR *pwsz) { HRESULT hr = S_OK; size_t cb = wcstombs(m_szName, pwsz, 1024); // check for buffer overflow or bad conversion // проверяем переполнение буфера или неверное преобразование if (cb == sizeof(m_szName) cb == (size_t)-1) { m_szName[0] = 0; hr = E_INVALIDARG; } return hr; } };
Этот код является довольно простым, хотя программист должен осознавать, что используются два различных типа символов. Несколько более сложный (и чаще встречающийся) случай — преобразование между типами данных OLECHAR и TCHAR из Win32. Так как OLECHAR условно компилируется как char или wchar_t, то при реализации метода необходимо должным образом рассмотреть оба сценария:
class BigDog : public ILabrador { TCHAR m_szName[1024]; // note TCHAR-based string // отметим строку типа TCHAR public: STDMETHODIMP SetName( /*[in,string]*/ const OLECHAR *pwsz) { HRESULT hr = S_OK; #ifdef UNICODE // Unicode build (TCHAR == wchar_t) // конструкция Unicode (TCHAR == wchar_t) wcsncpy(m_szName, pwsz, 1024); // check for buffer overflow // проверка на переполнение буфера if (m_szName[1023] != 0) { m_szName[0] = 0; hr = E_INVALIDARG; } #else // Non-Unicode build (TCHAR == char) // не является конструкцией Unicode (TCHAR == char) size_t cb = wcstombs(m_szName, pwsz, 1024); // check for buffer overflow or bad conversion // проверка переполнения буфера или ошибки преобразования if (cb == sizeof(m_szName) cb == (size_t)-1) { m_szName[0] =0; hr = E_INVALIDARG; } #endif return hr; } };
Очевидно, операции с преобразованиями OLECHAR в TCHAR значительно сложнее. Но, к сожалению, это самый распространенный сценарий при программировании в СОМ на базе Win32.
Одним из подходов к упрощению преобразования текста является применение системы типов C++ и использование перегрузки функций для выбора нужной строковой процедуры, построенной на типах параметров. Заголовочный файл ustring.h из приложения к этой книге содержит семейство библиотечных строковых процедур, аналогичных стандартным библиотечным процедурам С, которые находятся в файле string.h. Например, функция strncpy имеет четыре соответствующих процедуры, зависящие от каждого из параметров, которые могут быть одного из двух символьных типов (wchar_t или char):
// from ustring.h (book-specific header) // из ustring.h (заголовок, специфический для данной книги) inline bool ustrncpy(char *p1, const wchar_t *p2, size_t c) { size_t cb = wcstombs(p1, p2, c); return cb != c && cb != (size_t)-1; };
inline bool ustrncpy(wchar_t *p1, const wchar_t *p2, size_t c) { wcsncpy(p1, p2, c); return p1[c - 1] == 0; };
inline bool ustrncpy(char *p1, const char *p2, size_t c) { strncpy(p1, p2, c); return p1[c - 1] == 0; };
inline bool ustrncpy(wchar_t *p1, const char *p2, size_t c) { size_t cch = mbstowcs(p1, p2, c); return cch != c && cch != (size_t)-1; }
Отметим, что для любого сочетания типов идентификаторов может быть найдена соответствующая перегруженная функция ustrncpy, причем результат показывает, была или нет вся строка целиком скопирована или преобразована. Поскольку эти процедуры определены как встраиваемые (inline) функции, их использование не внесет никаких затрат при выполнении. С этими процедурами предыдущий фрагмент кода станет значительно проще и не потребует условной компиляции:
class BigDog : public ILabrador { TCHAR m_szName[1024]; // note TCHAR-based string // отметим строку типа TCHAR public: STDMETHODIMP SetName(/* [in,string] */ const OLECHAR *pwsz) { HRESULT hr = S_OK; // use book-specific overloaded ustrncpy to copy or convert // используем для копирования и преобразования // перегруженную функцию ustrncpy, специфическую для данной книги if (!ustrncpy(m_szName, pwsz, 1024)) { m_szName[0] = 0; hr = E_INVALIDARG; } return hr; } };
Соответствующие перегруженные функции для процедур strlen, strcpy и strcat также включены в заголовочный файл ustring.h.
Использование перегрузки библиотечных функций для копирования строк из одного буфера в другой, как это показано выше, обеспечивает лучшее качество исполнения, уменьшает размер кода и непроизводительные издержки программиста. Однако часто возникает ситуация, когда одновременно используются СОМ и API-функции Win32, что не дает возможности применить эту технику. Рассмотрим следующий фрагмент кода, читающий строку из элемента редактирования и преобразующий ее в IID:
HRESULT IIDFromHWND(HWND hwnd, IID& riid) { TCHAR szEditText[1024]; // call a TCHAR-based Win32 routine // вызываем TCHAR-процедуру Win32 GetWindowText(hwnd, szEditText, 1024); // call an OLECHAR-based СОМ routine // вызываем OLECHAR-процедуру СОМ return IIDFromString(szEditText, &riid); }
Допуская, что этот код скомпилирован с указанным символом С-препроцессора UNICODE; он работает безупречно, так как TCHAR и OLECHAR являются просто псевдонимами wchar_t и никакого преобразования не требуется. Если же функция скомпилирована с версией Win32 API, не поддерживающей Unicode, то TCHAR является псевдонимом для char, и первый параметр для IIDFromString имеет неправильный тип. Чтобы решить эту проблему, нужно провести условную компиляцию:
HRESULT IIDFromHWND(HWND hwnd, IID& riid) { TCHAR szEditText[1024]; GetWindowText(hwnd, szEditText, 1024); #ifdef UNICODE return IIDFromString(szEditText, &riid); #else OLECHAR wszEditText[l024]; ustrncpy(wszEditText, szEditText, 1024); return IIDFromString(wszEditText, &riid); #endif }
Хотя этот фрагмент и генерирует оптимальный код, очень утомительно применять эту технику всякий раз, когда символьный параметр имеет неверный тип. Можно справиться с этой проблемой, если использовать промежуточный (shim) класс с конструктором, принимающим в качестве параметра любой тип символьной строки. Этот промежуточный класс должен также содержать в себе операторы приведения типа, что позволит использовать его в обоих случаях: когда ожидается const char * или const wchar_t *.
В этих операциях приведения промежуточный класс либо выделяет резервный буфер и производит необходимое преобразование, либо просто возвращает исходную строку, если преобразования не требовалось. Деструктор промежуточного класса может затем освободить все выделенные буферы. Заголовочный файл ustring.h содержит два таких промежуточных класса: _U и _UNCC. Первый предназначен для нормального использования; второй используется с функциями и методами, тип аргументов которых не включает спецификатора const (таких как IIDFromString). При возможности применения двух промежуточных классов предыдущий фрагмент кода может быть значительно упрощен:
HRESULT IIDFromHWND(HWND hwnd, IID& riid) { TCHAR szEditText[1024]; GetWindowText(hwnd, szEditText, 1024); // use _UNCC shim class to convert if necessary // используем для преобразования промежуточный класс _UNCC, // если необходимо return IIDFromString(_UNCC(szEditText), &riid); }
Заметим, что не требуется никакой условной компиляции. Если код скомпилирован с версией Win32 с поддержкой Unicode, то класс _UNCC просто пропустит исходный буфер через свой оператор приведения типа. Если же код компилируется с версией Win32, не поддерживающей Unicode, то класс _UNCC выделит буфер и преобразует строку в Unicode. Затем деструктор _UNCC освободит буфер, когда операция будет выполнена полностью.
Следует обсудить еще один дополнительный тип данных, связанный с текстом, — BSTR. Строковый тип BSTR нужно применять во всех интерфейсах, которые предполагается использовать из языков Visual Basic или Java. Строки BSTR являются OLECHAR-строками с префиксом длины (length-prefix) в начале строки и нулем в ее конце. Префикс длины показывает число байт, содержащихся в строке (исключая завершающий нуль) и записан в форме четырехбайтового целого числа, непосредственно предшествующего первому символу строки. Рисунок 2.7 демонстрирует BSTR на примере строки "Hi". Чтобы позволить методам свободно возвращать строки BSTR без заботы о выделении памяти, все BSTR размещены с помощью распределителя памяти, управляемого СОМ.
В СОМ предусмотрено несколько API-функций для управления BSTR:
// from oleauto.h // allocate and initialize a BSTR // выделяем память и инициализируем строку BSTR
BSTR SysAllocString(const OLECHAR *psz); BSTR SysAllocStringLen(const OLECHAR *psz, UINT cch);
// reallocate and initialize a BSTR // повторно выделяем память и инициализируем BSTR INT SysReAllocString(BSTR *pbstr, const OLECHAR *psz); INT SysReAllocStringLen(BSTR *pbstr, const OLECHAR * psz, UINT cch);
// free a BSTR // освобождаем BSTR void SysFreeString(BSTR bstr);
// peek at length- prefix as characters or bytes // считываем префикс длины как число символов или байт UINT SysStringLen(BSTR bstr); UINT SysStringByteLen(BSTR bstr);
При пересылке строк методу в качестве параметров типа [in] вызывающий объект должен заботиться о том, чтобы вызвать SysAllocString прежде, чем запускать сам метод, и чтобы вызвать SysFreeString после того, как метод закончил работу. Рассмотрим следующее определение метода:
HRESULT SetString([in] BSTR bstr);
Пусть в вызывающей программе уже имеется строка, совместимая с OLECHAR, тогда для того, чтобы преобразовать строку в BSTR до вызова метода, необходимо следующее:
// convert raw OLECHAR string to a BSTR // преобразовываем "сырую" строку OLECHAR в строку BSTR BSTR bstr = SysAllocString(OLESTR("Hello")); // invoke method // вызываем метод HRESULT hr = p->SetString(bstr); // free BSTR // освобождаем BSTR SysFreeString(bstr);
Промежуточный класс для работы с BSTR, _UBSTR, включен в заголовочный файл ustring.h:
// from ustring.h (book-specific header file) // из ustring.h (специфический для данной книги заголовочный файл) class _UBSTR { BSTR m_bstr; public: _UBSTR(const char *psz) : m_bstr(SysAllocStringLen(0, strlen(psz))) { mbstowcs(m_bstr, psz, INT_MAX); } _UBSTR(const wchar_t *pwsz) : m_bstr(SysAllocString(pwsz)) { } operator BSTR (void) const { return m_bstr; } ~_UBSTR(void) { SysFreeString(m_bstr); } };
При наличии такого промежуточного класса предыдущий фрагмент кода значительно упростится:
// invoke method // вызываем метод HRESULT hr = p->SetString(_UBSTR(OLESTR("Hello")));
Заметим, что в промежуточном классе UBSTR могут быть в равной степени использованы строки типов char и wchar_t.
При передаче из метода строк через параметры типа [out] объект обязан вызвать SysAllocString, чтобы записать результирующую строку в буфер. Затем вызывающий объект должен освободить буфер путем вызова SysFreeString. Рассмотрим следующее определение метода:
HRESULT GetString([out, retval] BSTR *pbstr);
При реализации метода потребуется создать новую BSTR-строку для возврата вызывающему объекту:
STDMETHODIMP MyClass::GetString(BSTR *pbstr) { *pbstr = SysAllocString(OLESTR("Coodbye!")) ; return S_OK; }
Теперь вызывающий объект должен освободить строку сразу после того, как она скопирована в управляемый приложением строковый буфер:
extern OLECHAR g_wsz[]; BSTR bstr = 0; HRESULT hr = p->GetString(&bstr); if (SUCCEEDED(hr)) { wcscpy(g_wsz, bstr); SysFreeString(bstr); }
Тут нужно рассмотреть еще один важный аспект BSTR. В качестве BSTR можно передать нулевой указатель, чтобы указать на пустую строку. Это означает, что предыдущий фрагмент кода не совсем корректен. Вызов wcscpy:
wcscpy(g_wsz, bstr);
должен быть защищен от возможных нулевых указателей:
wcscpy (g_wsz, bstr ? bstr : OLESTR(""));
Для упрощения использования BSTR в заголовочном файле ustring.h содержится простая встраиваемая функция:
intline OLECHAR *SAFEBSTR(BSTR b) { return b ? b : OLESTR(""); }
Разрешение использовать нулевые указатели в качестве BSTR делает тип данных более эффективным с точки зрения использования памяти, хотя и приходится засорять код этими простыми проверками.
Простые типы, показанные на рис. 2.6, могут компоноваться вместе с применением структур языка С. IDL подчиняется правилам С для пространства имен тегов (tag namespace). Это означает, что большинство IDL-определений интерфейсов либо используют операторы определения типа (typedef):
typedef struct tagCOLOR { double red; double green; double blue; } COLOR;
HRESULT SetColor([in] const COLOR *pColor);
либо должны использовать ключевое слово struct для квалификации имени тега:
struct COLOR { double red; double green; double blue; };
HRESULT SetColor([in] const struct COLOR *pColor);
Первый вариант предпочтительней. Простые структуры, подобные приведенной выше, можно использовать как из Visual Basic, так и из Java. Однако в то время, когда пишется эта книга, текущая версия Visual Basic может обращаться только к интерфейсам, использующим структуры, но она не может быть использована для реализации интерфейсов, в которых структуры являются параметрами методов.
IDL и СОМ поддерживают также объединения (unions). Для обеспечения однозначной интерпретации объединения IDL требует, чтобы в этом объединении имелся дискриминатор (discriminator), который показывал бы, какой именно член объединения используется в данный момент. Этот дискриминатор должен быть целого типа (integral type) и должен появляться на том же логическом уровне, что и само объединение. Если объединение объявлено вне области действия структуры, то оно считается неинкапсулированным (nonencapsulated):
union NUMBER { [case(1)] long i; [case(2)] float f; };
Атрибут [case] применен для установления соответствия каждого члена объединения своему дискриминатору. Для того чтобы связать дискриминатор с неинкапсулированным объединением, необходимо применить атрибут [switch_is]:
HRESULT Add([in, switch_is(t)] union NUMBER *pn, [in] short t);
Если объединение заключено вместе со своим дискриминатором в окружающую структуру, то этот составной тип (aggregate type) называется инкапсулированным, или размеченным объединением (discriminated union):
struct UNUMBER { short t; [switch_is(t)] union VALUE { [case(1)] long i; [case(2)] float f; }; };
СОМ предписывает для использования с Visual Basic одно общее размеченное объединение. Это объединение называется VARIANT и может хранить объекты или ссылки на подмножество базовых типов, поддерживаемых IDL.
Каждому из поддерживаемых типов присвоено соответствующее значение дискриминатора:
VT_EMPTY nothing VT_NULL SQL style Null VT_I2 short VT_I4 long VT_R4 float VT_R8 double VT_CY CY (64-bit currency) VT_DATE DATE (double) VT_BSTR BSTR VT_DISPATCH IDispatch * VT_ERROR HRESULT VT_BOOL VARIANT_BOOL (True=-1, False=0) VT_VARIANT VARIANT * VT_UNKNOWN IUnknown * VT_DECIMAL 16 byte fixed point VT_UI1 opaque byte
Следующие два флага можно использовать в сочетании с вышеприведенными тегами, чтобы указать, что данный вариант (variant) содержит ссылку или массив данного типа:
VT_ARRAY Указывает, что вариант содержит массив SAFEARRAY VT_BYREF Указывает, что вариант является ссылкой
СОМ предлагает несколько API-функций для управления VARIANT:
// initialize a variant to empty // обнуляем вариант void VariantInit(VARIANTARG * pvarg);
// release any resources held in a variant // освобождаем все ресурсы, используемые в варианте HRESULT VariantClear(VARIANTARG * pvarg);
// deep-copy one variant to another // полностью копируем один вариант в другой HRESULT VariantCopy(VARIANTARG * plhs, VARIANTARG * prhs);
// dereference and deep-copy one variant into another // разыменовываем и полностью копируем один вариант в другой HRESULT VariantCopyInd(VARIANT * plhs, VARIANTARG * prhs);
// convert a variant to a designated type // преобразуем вариант к указанному типу HRESULT VariantChangeType(VARIANTARG * plhs, VARIANTARG * prhs, USHORT wFlags, VARTYPE vtlhs);
// convert a variant to a designated type // преобразуем вариант к указанному типу (с явным указанием кода локализации) HRESULT VariantChangeTypeEx(VARIANTARG * plhs, VARIANTARG * prhs, LCID lcid, USHORT wFlags, VARTYPE vtlhs);
Эти функции значительно упрощают управление VARIANT'ами. Чтобы понять, как используются эти API-функции, рассмотрим метод, принимающий VARIANT в качестве [in]-параметра:
HRESULT UseIt([in] VARIANT var);
Следующий фрагмент кода демонстрирует, как передать в этот метод целое число:
VARIANT var; VariantInit(&var); // initialize VARIANT // инициализируем VARIANT V_VT(&var) = VT_I4; // set discriminator // устанавливаем дискриминатор V_I4(&var) = 100; // set union // устанавливаем объединение HRESULT hr = pItf->UseIt(var); // use VARIANT // используем VARIANT VariantClear(&var); // free any resources in VARIANT // освобождаем все ресурсы VARIANT
Отметим, что этот фрагмент кода использует макросы стандартного аксессора (accessor) для доступа к элементам данных VARIANT. Две следующие строки
V_VT(&var) = VT_I4; V_I4(&var) = 100;
эквивалентны коду, который обращается к самим элементам данных:
var.vt = VT_I4; var.lVal = 100;
Первый вариант предпочтительнее, так как он может компилироваться на С-трансляторах, которые не поддерживают неименованных объединений.
Ниже приведен пример того, как с помощью приведенной выше технологии реализация метода использует параметр VARIANT в качестве строки:
STDMETHODIMP MyClass::UseIt( /*[in] */ VARIANT var) { // declare and init a second VARIANT // объявляем и инициализируем второй VARIANT VARIANT var2; VariantInit(&var2); // convert var to a BSTR and store it in var2 // преобразуем переменную в BSTR и заносим ее в var2 HRESULT hr = VariantChangeType(&var2, &var, 0, VT_BSTR); // use the string // используем строку if (SUCCEEDED(hr)){ ustrcpy(m_szSomeDataMember, SAFEBSTR(V_BSTR(&var2))); // free any resources held by var2 // освобождаем все ресурсы, поддерживаемые var2 VariantClear(&var2); } return hr; }
Отметим, что API-процедура VariantChangeType способна осуществлять сложное преобразование любого переданного клиентом типа из VARIANT в нужный тип (в данном случае BSTR).
Один из последних типов данных, который вызывает дискуссию, — это интерфейс СОМ. Интерфейсы СОМ могут быть переданы в качестве параметров метода одним из двух способов. Если тип интерфейсного указателя известен на этапе проектирования, то тип интерфейса может быть объявлен статически:
HRESULT GetObject([out] IDog **ppDog);
Если же тип на этапе проектирования неизвестен, то разработчик интерфейса может дать пользователю возможность задать тип на этапе выполнения. Для поддержки динамически типизируемых интерфейсов в IDL имеется атрибут [iid_is]:
HRESULT GetObject([in] REFIID riid, [out, iid_is(riid)] IUnknown **ppUnk);
Хотя эта форма будет работать вполне хорошо, следующий вариант предпочтительнее из-за его подобия с QueryInterface:
HRESULT GetObject([in] REFIID riid, [out, iid_is(riid)] void **ppv);
Атрибут [iid_is] можно использовать как с параметрами [in], так и [out] для типов IUnknown * или void *. Для того чтобы использовать параметр интерфейса с динамически типизируемым типом, необходимо просто установить IID указателя требуемого типа:
IDog *pDog = 0; HRESULT hr = pItf->GetObject(IID_IDog, (void**)&pDog);
Соответствующая реализация для инициализации этого параметра просто использовала бы метод QueryInterface для нужного объекта:
STDMETHODIMP Class::GetObject(REFIID riid, void **ppv) { extern IUnknown * g_pUnkTheDog; return g_pUnkTheDog->QueryInterface(riid, ppv); }
Для уменьшения количества дополнительных вызовов методов между клиентом и объектом указатели интерфейса с динамически типизируемым типом должны всегда использоваться вместо указателей интерфейса со статически типизируемым типом IUnknown.
1
Тип OLECHAR был предпочтен типу данных TCHAR, используемому Wn32 API, чтобы избежать необходимости поддержки двух версий каждого интерфейса (CHAR и WCHAR). Поддерживая только один тип символов, разработчики объектов становятся независимыми от типов символов препроцессора UNICODE, который используется их клиентами.
2
_UNCC является просто версией _U и имеет операторы приведения типа для wchart * и char *. Хотя расширенный вариант можно использовать где угодно, автор предпочитает использовать его только при согласовании с непостоянно корректными интерфейсами, чтобы подчеркнуть, что система типов в некоторой степени компрометируется. Увы, многие из СОМ API не являются постоянно корректными, так что промежуточный класс _UNCC применяется очень часто.
3
Хотя автор и находит строковые процедуры из ustring.h более чем подходящими для управления обработкой текстов в СОМ, библиотеки ATL и MFC используют несколько иной подход, основанный на аllоса и макросах. Более подробную информацию об этих подходах можно прочитать в соответствующей документации.
4
К нему можно обращаться и как к VARIANTARG.Термин VARIANTARG относится к вариантам, которые являются допустимыми типами параметров. Термин же VARIANT относится к вариантам, которые являются допустимыми результатами методов. Тип данных VARIANTARG является просто псевдонимом для VARIANT, и оба этих типа могут использоваться равнозначно.