пятница, 23 апреля 2010 г.

Записки уникодера

Проект шагнул за пределы РФ, всвязи с чем встал вопрос о переходе на unicode. Ну это так, для начала. Там кроме уникода еще много веселого и интересного будет. Проект большой, свыше тысячи юнитов (pas - модулей), что-то там около 600-700 тысяч строк паскаль-кода, про хранимые процедуры скромно умолчу. Проекту много лет, был написан еще на Delphi5. Сейчас как промежуточный шаг, перешли на Delphi7 (были некоторые проблемы, но мы их решили). Следующий рубеж - Delphi 2009, или скорее уже Delphi 2010. И главная проблема, конечно же - UNICODE. Она же - наша цель, то ради чего и задуман переход.

Итак, что же стоит за этой "уникодизацией"? Звучит то вроде бы достаточно безобидно. Прежде всего, самое главное - SizeOf(Char) теперь не равен SizeOf(Byte). А string теперь содержит не однобайтовые символы, а двухбайтовые.

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

Итак, по порядку.

Проблемы, которые обнаруживают себя сами, при компиляции:

1) Проблема с конструкцией Char in ['a', 'b', 'c']

Тип Char больше не может быть элементом множества, поскольку множество имеет ограничение на 256 элементов. Вместо этой конструкции можно использовать новую функцию в модуле SysUtils - CharInSet

2) Format, UpperCase, LowerCase

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

3) SetString

Функция SetString не имеет аналога для AnsiString. Выкручивайтесь, как хотите. Мы написали свою функцию:

function StrFromBuffer(Buffer: PAnsiChar; Length: Cardinal): AnsiString; overload;
begin
  SetLength(Result, Length);
  CopyMemory(PAnsiChar(Result), Buffer, Length);
end;

{$IFDEF UNICODE}
function StrFromBuffer(Buffer: PWideChar; Length: Cardinal): UnicodeString; overload;
begin
  SetString(Result, Buffer, Length);
end;
{$ENDIF}

4) TDataSet.Bookmark

Свойство Bookmark выдает теперь значение другого типа. Фактически новый вариант несовместим со старым.
Проблема решается заменой на GetBookmark, GotoBookmark, FreeBookmark - работает как в D5/D7, так и в Delphi2009

Проблемы, которые приходится искать

1) Chr

Фукнция Chr ожидает теперь на входе двухбайтовый код unicode-символа, на выходе дает UNICODE же символ. Функция Chr более не позволяет получить символ по его ASCII-коду (разве что для английский букв). Для этой цели можно использовать преобразование AnsiChar(). Однако полученный AnsiChar нельзя приводить к Char напрямую, его нужно вначале записать в AnsiString и только потом преобразовать AnsiString к string для перекодирования символа в UNICODE

Обнаружение: Поиск по Chr(

Решение: Для получения символа по его ASCII-коду использовать приведение типа AnsiChar(ASCII-код) и переменные типа AnsiChar, AnsiString

2) Ord

Функция Ord работает по-разному в зависимости от типа параметра (Char или AnsiChar). ASCII код можно получить, только если параметр имеет тип AnsiChar. Однако напрямую Char к AnsiChar приводить нельзя, поскольку при этом не происходит конвертация UNICODE в 8-битную кодировку. Нужно вначале привести string к AnsiString для перекодирования, затем взять символ из AnsiString.

Обнаружение: Поиск по Ord(

Решение: Для определения ASCII-кода символа использовать переменные типа AnsiChar и AnsiString

3) PChar(Pointer)

Подобное приведение типа возможно требует корректировки на PAnsiChar(Pointer). Здесь нужно разобраться, что за буфер, что он хранит на самом деле, 8-битные символы или уникод? Если уникод, то следует проверить, правильно ли определяется размер буфера, правильно ли выделяется под него память, ведь уникод-символы занимают в два раза больше места.
Обнаружение: поиск по PChar(
 
4) SizeOf (Buffer)

При вызове некоторых API функций ожидается размер буфера в символах. При этом для его определения используется оператор SizeOf, который выдает размер в байтах. Теперь символ не равен байту, поэтому размер буфера определяется неправильно. Хотя (такое тоже бывает) некоторые API-функции требуют размер все-таки в байтах, но там могут быть другие засады.
Обнаружение: Поиск по коду оператора SizeOf
Рекомендуемое решение: Заменять буфер на string, а SizeOf - на Length там, где требуется символьный буфер (например при вызове Unicode-версий функций Windows API)

5) RegQueryValue, RegQueryValueEx

Функции, в отличие от большинства апишных функций, ожидают размер буфера в байтах, а не в символах. Если вы в качестве размера буфера указали результат функции Length от строки, то у вас будут проблемы, потому что Length выдает длину строки в символах
Обнаружение: Поиск по имени функции
Решение: Использовать оператор SizeOf вместо Length, либо домножить Length на SizeOf(Char). И проверить, правильно ли выделена память под буфер (ее теперь требуется в два раза больше)

6) StrBuffer[1]

Конструкции наподобие StrBuffer[1] часто используются для передачи ссылки на строку в функции, использующие нетипизированные буферы. Такое использование строки как буфера наверняка требует корректировки. Вообще нетипизированные буферы - источник больших проблем. Все случаи их использования нужно внимательно проанализировать. Возможно потребуется конверсия уникодной строки в ANSI, либо нужно будет поправить параметр, отвечающий за размер буфера.
Обнаружение: Поиск по [1]

7) FillChar, Move

Функции FillChar, Move работают с байтами и не могут без внесения изменений использоваться для строк.
Обнаружение: Поиск по FillChar, Move(
Решение: Замена на StringOfChar или коррекция размера буфера с учетом уникода

8) GetMem, ReallocMem

Функции GetMem нельзя более передавать результат выполнения Length в качестве размера выделяемого блока памяти
Обнаружение: Поиск по GetMem, ReallocMem
Решение: использовать SizeOf для array[...] of Char, либо Length домноженный на SizeOf(Char) для строк.

9) TVarRec

Если используется структура TVarRec для приема открытого массива нетипизированных параметров вида "Args: array of const", то нужно обработать новые типы значений для Unicode-строк
Обнаружение: Поиск по TVarRec
Встречается крайне редко, но у нас такой код есть (у нас вообще много диковинного кода)

10) ReadFile, WriteFile

Функции Windows API ReadFile и WriteFile читают/пишут байты. При чтении/записи строковой информации в кодировке ANSI следует использовать AnsiString или массивы AnsiChar в качестве буфера, а не string/Char.
Обнаружение: Поиск по ReadFile, WriteFile

11) TStream (ReadBuffer, Read, WriteBuffer, Write)

Возможно требуется корректировка размеров буфера, а так же замена на AnsiString.
Обнаружение: Поиск по ReadBuffer/WriteBuffer, поиск по [1]

12) FormatBuf(var Buffer; BufLen: Cardinal...)

Функция ожидает буфер, заполненный 8-битными символами. Она получила overload-версии, ожидающие PWideChar, однако компилятор не всегда выбирает правильный вариант для вызова. По-хорошему, функцию нужно было объявить как deprecated, так как ей невозможно сделать нормальную перегрузку.
Обнаружение: поиск по FormatBuf
Решение: Условная компиляция и замена на WideFormatBuf. Или просто замена на Format, FmtStr, StrFmt

13) ExceptionErrorMessage, StrLFmt, StrLCopy

Функции ожидают на входе размер не в байтах, а в символах:
Обнаружение: Поиск по имени функции

14) TStringStream

Класс теперь поддерживает различные кодировки. Для чего получил кучу разных конструкторов на все случаи жизни.
Необходимо убедиться, что используется правильная кодировка. Кодировке ANSI соответствует TEncoding.Default
Обнаружение: Поиск по TStringStream

15) OemToChar, CharToOem, OemToCharBuff, CharToOemBuff

Данные функции API в UNICODE версии работают иначе чем в ANSI. ANSI версия допускает, что входной буфер совпадает с выходным. Чем, как правило, и пользуются: OemToChar(PChar(str), PChar(str)). UNICODE версия такого не допускает - выходной буфер должен быть в два раза больше входного. Поэтому, целесообразно явно вызывать ANSI-версию: OemToCharA
Обнаружение: Поиск по OemToChar, CharToOem, OemToCharBuff, CharToOemBuff
Решение: Явный вызов ANSI-функций OemToCharA, CharToOemA, OemToCharBuffA, CharToOemBuffA