Строки в VB6
Часть 1: эмоциональная
Строка и байтовый массив – одно и то же? только не в VB6!
Вы, наверное, думали, что строка и байтовый массив – одно и то же, раз их даже можно присваивать в обе стороны? Я тоже так думал.
Пока мы пользуемся голым VB6, мы можем хранить в строке любую гадость. Недопустимых символов в Юникоде достаточно много, но (к счастью) VB не станет “по собственной инициативе” проверять корректность строки – до тех пор, пока он не будет вынужден это сделать при работе со строковыми функциями типа IsNumeric. Даже и при этом самое худшее, что может произойти – поломка его “искусственного интеллекта”; исправлять ошибки в строке он не решится.
Всё меняется, когда в смесь добавляются API. Как мы все убедились, VB6 уверен, что все API принимают и возвращают исключительно ANSI-строки – это при том, что внутри VB строки хранятся исключительно в Юникоде. У нас нет никакого шанса избежать двух конвертаций (одна по дороге туда, вторая – обратно) при передаче строки в качестве параметра, объявленного As String. Есть множество обходных путей (передавать StrPtr как ByVal As Long, объявлять функцию в TLB и т.д.), которые не рассматриваются в этой части статьи. Важен факт: передача строки As String – это пара конвертаций, сначала из Юникода в ANSI, потом обратно. Что, если нам нужно передать в API строку в виде Юникода? Лобовое решение – передавать StrConv(vbUnicode); получается строка “в кодировке Double Secret Unicode”, которая затем подвергается названной паре преобразований. Итог для передачи юникодных строк As String – три преобразования для входного параметра и четыре для выходного. Неплохо для языка, в котором Юникод – родная кодировка?
Теперь – самое интересное. Преобразование из Юникода в ANSI необратимо – поэтому на выходе API мы гарантированно получаем испорченную строку. На самом деле это из-за того, что API сама получила испорченную строку на входе. Это не так уж разрушительно: если API по своей природе принимает ANSI-строку, то она всё равно способна обрабатывать в строке только те символы Юникода, которые есть в ANSI-кодировке. А что, если преобразование ANSI→Юникод→ANSI нетождественное? (Так оно и есть в некоторых восточноазиатских кодировках с MBCS, “ведущими байтами”, “нормализацией радикалов” и другими страшными словами.) Тогда преобразование Юникод→Двойной Юникод→Юникод тоже будет нетождественным, и наша API, принимающая юникодную строку, получит мусор – даже хотя она была способна обработать любые символы Юникода. Самое милое в этом баге – то, что его эффекты меняются в зависимости от системной локали, и вовсе не проявляются в европейских локалях, в которых преобразование ANSI→Юникод→ANSI тождественное. Теперь представим себе, что рассматриваемая API – это CallWindowProc(ByVal As String, ByVal As Long, ByVal As Long, ByVal As Long, ByVal As Long). Мы вызываем её следующим образом:
CallWindowProc Chr(&H14)+Chr(&H76)+Chr(&H55)+Chr(&HD0)+Chr(&HF8), 0,0,0,0
(собственно, строка с кодом взята с потолка, не обращайте на неё внимания)
При вызове Chr происходит преобразование из ANSI в Юникод, при вызове CallWindowProc – обратно. С большой вероятностью по дороге сломается байт-другой; результат – фатальный вылет в восточноазиатских локалях. Такой баг можно было бы выискивать годами: никому не придёт в голову, что вызываемый фрагмент ассемблерного кода как-то связан с языковыми настройками машины.
Как этот код можно починить? Близорукий европеец, наверное, заменил бы Chr на ChrW: ChrW не выполняет преобразования кодировок, значит, по идее, не будет и “круга с подвохом”. И действительно, у этого европейца такой код будет работать:
CallWindowProc ChrW(&H14)+ChrW(&H76)+ChrW(&H55)+ChrW(&HD0)+ChrW(&HF8), 0,0,0,0
В чём баг теперь? Символы, которые произведёт на свет ChrW, будут заключены между U+0000 и U+00FF. Символов старше U+007F (расширенная латиница) гарантированно не окажется ни в одной восточноазиатской кодировке: да что там, их даже в 1251 нет. Значит, теперь на этапе преобразования из Юникода в ANSI при вызове CallWindowProc похерятся все байты старше &H7F. Баг не только не исправлен: он усугублен.
А истинное решение – не пытаться перехитрить VB6, и для хранения байтовых массивов использовать именно байтовые массивы. Уж с ними-то VB6 такие вольности себе не позволяет.
P.S.: в комментариях к посту Майкла Каплана, на который я сослался выше, ругают Мэтта Курланда, который догадался передавать ассемблерный код в строковой переменной. На самом деле, Курланд догадался ещё и не до такого – код короче 8 байт он передаёт RyRef As Currency :-))
Часть 2: теоретическая
Мэтт Курланд, Advanced VB6. Краткое содержание главы 14 "строки в VB"
Здесь я просто перечисляю тезисы той главы, обильно снабжая их собственными комментариями. Искренне верю, что это не нарушение копирайта :-)
-
Строки в VB6 бывают с двумя разными “ароматами”: стандартный BSTR (широкая строка, предварённая 4-байтной длиной, и с нулевым символом в конце) и “ABSTR” – то же самое, но в кодировке ANSI (в частности, длина такой ABSTR-строки может быть нечётной). Пустая строка ("") и нулевая строка (vbNullString) считаются эквивалентными. Единственный способ их различить – проверять StrPtr на равенство нулю. Поскольку длина строки хранится явно, функция Len выполняется мгновенно. В частности, проверять длину строки на равенство нулю – быстрее, чем проверять строку на равенство пустой/нулевой. При копировании строки в байтовый массив нулевой символ в конце не копируется; при копировании байтового массива в строку нулевой символ в конце добавляется автоматически.
Преобразования между BSTR и ABSTR выполняются в очень широком классе случаев, и всегда используют системную локаль по умолчанию. Задать используемую локаль явно – невозможно, но возможно самому выполнять необходимые преобразования, и постараться избежать автоматических преобразований, выполняемых VB6. (Тут Курланд ссылается на книгу Каплана, на которую Каплан ссылается сам по вышеприведённой ссылке ;-)
Чтобы передать юникодную строку в API, нужно передавать StrPtr как ByVal As Long. Это единственный способ, позволяющий избежать копирования строк взад-вперёд – неизбежного при использовании функций WideChar↔MultiByte и StrConv, и уж тем более при конвертировании руками, в цикле, по одному символу :-)) Чтобы передать UDT с юникодной строкой в API, нужно передавать VarPtr(UDT) как ByVal As Long. Как мы видим, при передаче в API юникодных строк всё достаточно просто: все проблемы решаемы. Всякие ренегады, утверждающие, что работа с Юникодом неизбежно требует применения TLB, заблуждаются. Тогда, когда одна и та же строка передаётся в API много раз подряд, выгодно избегать неявного преобразования ANSI→Юникод даже в том случае, если API ожидает именно ANSI-строку. В этом случае лучше один раз записать в строку (или байтовый массив) StrConv(vbFromUnicode), и затем каждый раз передавать StrPtr.
В TLB допускаются строки трёх видов: BSTR, LPSTR и LPWSTR. Таких чудищ, как ABSTR и “Double Secret Unicode”, в COM нет – эти типы строк существуют только внутри VB6; но VB6 умеет правильно работать со всеми тремя типами COM-овских строк. Все эти три типа строк, когда к проекту подключена использующая их TLB, в Object Browser совершенно неразличимы. Применение TLB позволяет непосредственно объявить API как принимающую или возвращающую юникодные строки. В частности, именно так объявлена сама StrPtr. (Надеюсь, ни для кого не новость, что VarPtr, StrPtr и ObjPtr – это три разных объявления одной и той же самой функции?) В структурах внутри TLB можно объявить только строки типа BSTR – нельзя объявлять даже строки фиксированной длины, которые успешно объявляются непосредственно в VB6. Так что при передаче строк в API в составе UDT от TLB нет никакой помощи.
Часть 3: исследовательская
Возврат строк из API и восстановление строки по указателю
Именно ради этой части и затевалась статья. Теперь, когда под использование строк в VB подведена солидная теоретическая база, можно с достаточной уверенностью обсуждать основную проблему: нужна ли нам CopyMemory для восстановления строки по указателю?
Получением строки (BSTR) по указателю (LPTSTR) занимаются API SysAllocString, SysAllocStringLen и SysAllocStringByteLen; две первые возвращают настоящие BSTR-строки, последняя – BSTR либо ABSTR, в зависимости от переданных данных. (Различие между BSTR и ABSTR вообще достаточно условное, и хранится не в памяти компьютера, а в памяти программиста.)
Когда API объявлена как As String, VB считает, что она возвращает ABSTR. (Интересно, есть ли в природе хоть одна API, которая действительно возвращает ABSTR?) Есть возможность объявить API в TLB как BSTR или LPWSTR. Тогда, например, после вызова не будет выполняться неявная конвертация возвращённой строки из ANSI в Юникод – особенно досадная тогда, когда возвращённая строка уже в Юникоде. Возможности принять на выходе API строку типа LPTSTR без использования TLB нет: приходится объявлять такую API как As Long, и дальше действовать описанными в этой части статьи способами. Как видите, с TLB всё намного проще.
Предположим, у нас есть в переменной типа Long указатель на юникодную строку, т.е. LPWSTR. Как заполучить саму эту строку? Вот один из вариантов – StrConv(SysAllocString, vbFromUnicode). Тогда BSTR на выходе SysAllocString будет неявно преобразована в “Double Secret Unicode”, и затем явно – обратно в Юникод. Это не только криво, но ещё и бажно (см. первую часть статьи). Самый правильный вариант – пользоваться голой SysAllocString, объявленной в TLB как возвращающей BSTR. Тогда не будет ни одного преобразования кодировок, и ни одного копирования строк (кроме того, которое выполняется внутри самой SysAllocString). Чуть хуже – выделение строкового буфера размером lstrlenW, и копирование в него строки по CopyMemory(StrPtr,,lstrlenW). Это всё то же самое, что делает внутри себя SysAllocString – но она написана не на VB6, и явно будет поэффективнее такого же по сути самопального кода.
Если наш указатель на юникодную строку – уже BSTR, то нам не нужно ни одного копирования строк: достаточно скопировать сам этот указатель в переменную типа String. Но чего-то за всё время мне ни разу не приходилось восстанавливать BSTR-строку по указателю: да и как такой указатель мог попасть в нашу программу? Только из какой-нибудь API, или как параметр нашей callback-функции; в обоих случаях достаточно просто переопределить такой параметр как As String, и не иметь никаких проблем с восстановлением строк.
Последний возможный случай – восстановление ANSI-строки по LPSTR. Здесь неявное преобразование строки, возвращаемой из API, играет нам на руку, но мы уже не можем пользоваться SysAllocString, потому что в конце нашей строки нет завершающего нулевого слова. Выход – использование SysAllocStringByteLen с заранее высчитанной длиной строки: SysAllocStringByteLen(lstrlenA). Теперь уже нет ни одного лишнего копирования строк, и ни одной лишней конвертации кодировок – и при этом мы не используем ни TLB, ни CopyMemory!
Заключение
Поддержка Юникода в VB6 как будто бы есть: хранение и обработка юникодных строк допускается безо всяких ограничений.
Хотя основа VB6 – технология COM/ActiveX – также позволяет передавать в свойствах и методах классов юникодные строки без ограничений, ни один из контролов в стандартной поставке VB6 этой возможностью не пользуется. Чтобы вводить и выводить юникодные строки, нужно либо заморачиваться с API, либо пользоваться контролами третьих фирм (платными и/или бажными).
Передача и получение юникодных строк при вызовах API – осуществимы, хотя и сопряжены с определёнными сложностями. Во всех случаях, где это возможно, передачи строк в API как As String следует избегать.
При использовании юникодных строк в API дополнительные возможности, предоставляемые поддержкой TLB, позволяют во многих случаях избежать головной боли с явными и неявными конвертациями между многочисленными форматами строк, используемыми в VB6.
Возвращение строк из API ещё тяжелее и запутаннее, чем передача строк им на вход. Единственный тип строк, который VB6 позволяет получить из API напрямую – это ABSTR, который ни одна из реально существующих API не возвращает. Использование TLB позволяет существенно расширить возможности VB по приёму строк из API.
Когда для строки известен указатель, получить её саму не составляет труда. Это можно сделать и без использования CopyMemory, причём только в одном конкретном случае (восстановление строки по LPWSTR, когда использование TLB по каким-то причинам невозможно) использование CopyMemory имеет преимущество перед использованием SysAllocString*. В то же время использование для этой цели API типа WideCharToMultiByte, появляющееся в примерах из API-Guide, безоговорочно проигрывает всем остальным вариантам.
Copyright © 1999-2008 Visual Basic Streets
|
|