Антон Григорьев дата публикации 10-06-2008 09:08 урок из цикла:
Урок 10. Строки в COM/DCOM
В различных языках программирования используются разные форматы для хранения строк. Поэтому для передачи строк в COM/DCOM потребовался универсальный, не зависящий от языка формат.
Разумеется, ничто не мешает программисту, разрабатывающему сервер на Delphi, использовать, например, строки PChar, завершающиеся нулём. AnsiString и ShortString тоже, в принципе, можно использовать, но это предъявляет дополнительные требования к способу маршалинга (передаче данных из адресного пространства сервера в адресное пространство клиента). Кроме того, не на всяком языке можно написать клиент для работы с такими типами данных — для этого необходима либо их поддержка на уровне компилятора, либо возможности операций с указателями и явного перераспределения памяти, а эти средства есть далеко не во всех языках программирования.
Чтобы подобных проблем не возникало, для передачи строковых параметров следует использовать тип BSTR, который является стандартным строковым типом в COM/DCOM. Все компиляторы, поддерживающие COM/DCOM, обязаны иметь средства для низкоуровневой работы с этим типом и/или его высокоуровневую поддержку.
BSTR расшифровывается как Basic STRing. Такое название он получил потому, что в Visual Basic именно так хранятся строки. Переменная типа BSTR — это указатель на строку символов, причём тип символов зависит от платформы. Так, в Windows BSTR содержит 16-битные символы в кодировке Unicode, а в MacOS — 8-битные. При взаимодействии клиента и сервера, работающих на разных платформах, заглушка и заместитель (о них мы поговорим далее) обеспечивают трансляцию строк из одной кодировки в другую.
По отрицательному смещению от указателя типа BSTR хранит хранится 32-разрядное целое число — длина строки в байтах (а не в символах!). Это позволяет строке содержать нулевые символы в середине. В конце строки тоже должен быть нулевой символ. Это напоминает реализацию типа AnsiString в Delphi с той разницей, что BSTR не имеет счётчика ссылок, поэтому копирование только при необходимости для этого типа не реализовано.
Память для строк типа BSTR должна выделяться с помощью специальных системных функций (SysAllocString, SysReAllocString, SysAllocStringLen и т.п.), а освобождаться — с помощью функции SysFreeString. Для определения длины строки следует использовать функцию SysStringLen. В Delphi эти функции объявлены в модуле ActiveX.
Тип BSTR в Delphi называется TBStr и объявлен он также в модуле ActiveX. Это просто указатель, другое название типа PWideChar. Все операции по выделению и освобождении памяти, копированию строк и т.п. при использовании TBStr должны выполняться вручную.
В Delphi также существует более удобный тип для работы со строками COM/DCOM — это стандартный тип WideString, объявленный в модуле System. При использовании этого типа компилятор неявно вызывает функции SysXXXString для выделения памяти, копирования, сравнения и т.п. Когда переменная типа WideString выходит из области видимости, компилятор автоматически финализирует её, вызывая SysFreeString.
Чтобы понять разницу между TBStr и WideString, рассмотрим несколько примеров. Будем предполагать, что у нас объявлены переменные B1 и B2 типа TBStr и W1 и W2 типа WideString.
B1 := AllocSysString('SomeString');
B2 := B1;
W1 := 'SomeString';
W2 := W1;
|
|
Переменной B1 просто присвоить строковую константу нельзя — необходимо явно выделить память для этой строки (которую потом, естественно, так же явно придётся освобождать). При присвоении строковой константы переменной W1 компилятор сам позаботится о выделении для неё памяти, причём сделает это так, чтобы была совместимость с системным BSTR, т.е. через функции для работы с этим типом (в частности, седьмая версия Delphi использует функцию SysReAllocStringLen).
Присвоение B2 := B1 не приводит ни к какому перераспределению памяти — мы просто получаем ещё один указатель, указывающий на ту же самую строку, что и первый. А вот присвоение W2 := W1 приводит к неявному выделению памяти для новой строки и копировании туда старой строки. После выполнения этого присваивания Pointer(W1) <> Pointer(W2).
Оператор сравнения для WideString сравнивает именно строки, а не указатели. Рассмотрим это на конкретном примере.
B1 := AllocSysString('SomeString');
B2 := AllocSysString('SomeString');
if B1 = B2 then ...
W1 := 'SomeString';
W2 := 'SomeString';
if W1 = W2 then ...
|
|
В первом случае условие в операторе if будет иметь значение False, потому что B1 и B2 сравниваются как указатели, без учёта содержимого, а они указывают на две разные строки. А вот сравнение W1 и W2 даст True, потому что будет сравниваться именно содержимое строк, а не то, в каком месте памяти они лежат. (При этом, естественно, Pointer(W1) <> Pointer(W2).)
Можно также сравнивать WideString с TBStr, и при этом будет сравниваться содержимое, а не указатели. Так, в вышеприведённом примере сравнение B1 и W1 даст True.
В подавляющем большинстве случаев в Delphi проще использовать WideString, чем TBStr. Исключения бывают в тех случаях, когда для работы со строками используются указатели, имеющие тип, отличный от WideString. Пусть, например, приходится реализовывать метод, имеющий следующий прототип:
function SomeMethod(pBStr: PPointer): HRESULT;
|
|
Здесь подразумевается, что pBStr — это указатель на значение типа BSTR, но формально он объявлен как указатель на нетипизированный указатель (для программиста, привыкшего к Delphi, такая конструкция выглядит, по меньшей мере, странно, но когда приходится реализовывать интерфейсы, написанные другими людьми, привыкшими к другим языкам, можно столкнуться и не с такими странностями). И пусть сервер при выполнении данного метода должен создать строку и поместить указатель на неё в pBStr. Рассмотрим гипотетическую реализацию такого метода:
function SomeMethod(pBStr: PPointer): HResult;
var
ResStr: WideString;
begin
ResStr := 'SomeString';
pBStr^ := Pointer(ResStr);
Result := S_OK
end;
|
|
При завершении этого метода компилятор финализирует переменную ResStr, и указатель pBStr^ потеряет свою актуальность. В результате клиент (или заглушка) получит некорректный указатель, что приведёт к ошибке. Чтобы такое не произошло, в данном случае следует отказаться от использования WideString и вызывать функции распределения памяти для строк вручную.
Другой способ решения данной проблемы заключается в изменении прототипа метода таким образом, чтобы на двоичном уровне он оставался совместимым с исходным прототипом, но компилятор понимал, с каким типом данных он на самом деле работает. В данном случае прототип можно сделать таким:
function SomeMethod(out Str: WideString):HResult;
|
|
Этот способ достаточно удобен, но пользоваться им нужно с осторожностью, потому что некорректное изменение прототипа может нарушить совместимость.
[Строки] [Технологии ActiveX, COM, DCOM]
Обсуждение материала [ 11-06-2008 04:25 ] 5 сообщений |