Дата публикации статьи: 14.06.2006 19:04

Строки в 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 :-D

Часть 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, и уж тем более при конвертировании руками, в цикле, по одному символу :-D

    Чтобы передать 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, безоговорочно проигрывает всем остальным вариантам.