Unicode Support in Lazarus/ru

From Free Pascal wiki
Jump to: navigation, search

English (en) 日本語 (ja) русский (ru)

Введение

Здесь описывается поддержка Unicode в «программах» Lazarus (консольных или серверных, без графического интерфейса) и «приложениях» (GUI с использованием LCL) при использовании особенностей FPC 3.0+.

Старый метод поддержки UTF-8 в LCL при использовании FPC версий до 2.6.4 включительно, описан здесь: LCL Unicode Support

RTL с кодовой страницей UTF-8 по умолчанию

По умолчанию RTL использует системную кодовую страницу для AnsiStrings (в таких операциях, как например FileExists и TStringList.LoadFromFile). Под Windows это не-Unicode кодировка, поэтому могут использоваться только символы из текущей языковой группы (не более 256 символов). LCL, с другой стороны, работает с кодировкой UTF-8, охватывающей весь диапазон Unicode. Под Linux и Mac OS X UTF-8 обычно является системной кодовой страницей, и здесь RTL использует по умолчанию CP_UTF8.

FPC, начиная с версии 3.0, обеспечивает API для изменения кодовой страницы RTL по умолчанию на другое значение. Lazarus (конкретно пакет LazUtils) использует данный API и изменяет кодовую страницу RTL по умолчанию на UTF-8 (CP_UTF8). Это означает, что пользователи Windows также могут теперь использовать строки UTF-8 в RTL.

  • Например, FileExists и StringList.LoadFromFile(Filename) теперь имеют полную поддержку Unicode. См. Полный перечень функций, которые полносттью поддерживают Unicode, здесь:

RTL changes

  • AnsiToUTF8, UTF8ToAnsi, SysToUTF8, UTF8ToSys не работают (не изменяют передаваемых данных). Эти функции обычно использовались для упомянутых выше функций RTL, которые больше не нуждаются в преобразованиях. Относящееся к функциям WinAPI, см ниже.
  • Многочисленные вызовы UTF8Encode и UTF8Decode больше не требуются, потому что такие действия при присваивании значений UnicodeString переменным типа String и наоборот компилятор делает автоматически.
  • При работе с WinAPI необходимо использовать "W"-функции или пользоваться функциями UTF8ToWinCP и WinCPToUTF8. То же верно для библиотек, которые до сих пор используют Ansi WinAPI функции. Например, в FPC 3.0 и более ранних версиях в этом нуждается unit registry.
  • "String" и "UTF8String" — различные типы. Если вы присваиваете значение String переменной типа UTF8String, компилятор добавляет код для проверки совпадения кодировок. Это будет стоить дополнительного времени исполнения и увеличит размер кода. Просто используйте String вместо UTF8String.
  • Консоль Windows использует кодировку, которая может отличаться от системной (в случае русского языка это всегда именно так). writeln в FPC 3.0+ автоматически преобразует строки UTF-8 в кодовую страницу консоли. Некоторые консольные программы Windows ожидают на входе строки в кодировке консоли и результаты своей работы также выдают в кодовой странице консоли. Для соответствующего преобразования можно использовать функции UTF8ToConsole и ConsoleToUTF8.


Дополнительная информация о новинках поддержки Unicode в FPC: FPC Unicode support

Применение в Lazarus

Новый режим включается автоматически при компиляции FPC 3.0+. Это поведение может быть отключено директивой компиляции -dDisableUTF8RTL, детали см. на странице Lazarus with FPC3.0 without UTF-8 mode.

Если вы используете строковые литералы в новом режиме, ваши исходники всегда должны быть в кодировке UTF-8. Однако, ключ -FcUTF8 на самом деле обычно не требуется. Дополнительные сведения приведены ниже, в разделе "Строковые литералы".

Что же на самом деле происходит в новом режиме? В секции предварительной инициализации вызываются две FPC функции, устанавливающие кодировку строк по умолчанию в исполняющих библиотеках FPC в UTF-8 :

 SetMultiByteConversionCodePage(CP_UTF8);
 SetMultiByteRTLFileSystemCodePage(CP_UTF8);

Кроме того, функции UTF8...() из LazUTF8 (LazUtils) устанавливаются в качестве функций обратного вызова для функций RTL, имена которых начинаются с Ansi...().

Для консольных программ (без LCL), зависимость от LazUtils должна быть добавлена вручную. (Приложения LCL уже имеют эту зависимость через зависимость от LCL.)

Использование UTF-8 в программах без LCL

Если необходимо использовать новые строки UTF-8 в программах, не использующих LCL, добавьте unit LazUTF8 в секцию uses главного файла программы. Модуль должен находиться в самом начале списка, сразу после критичных менеджеров памяти и управления потоками (таких как cmem, heaptrc, cthreads).

Совместимость с Unicode Delphi

Для консольных программ, unit LazUTF8 должен присутствовать в секции uses главного файла программы. В Delphi такого unit'а нет.

RTL функции ASCII диапазона

RTL функции, работающие в ASCII диапазоне (такие как UpperCase) совместимы, но работают быстрее в UTF-8 RTL. В Delphi все строковые функции замедлены, после того как были переключены на UTF-16.

RTL функции Ansi...() Unicode

RTL фунции Ansi...() , работающие с кодовыми страницами / Unicode (такие как AnsiUpperCase), совместимы.

Чтение индивидуальных кодовых точек

Не совместимо, хотя довольно просто создать код, работающий с обеими кодировками.

В Delphi есть такие функции, как NextCharIndex, IsHighSurrogate, IsLowSurrogate, назначением которых является работа с суррогатными парами UTF-16, кодовыми точками, состоящими из двух UnicodeChar(*) (WideChar, Word, 2-байтовые). Однако, эти функции не упоминаются ни в одном примере кода в большинстве учебников. Большинство учебников заявляют, что функция Copy() работает так же, как было в версиях Delphi, более ранних чем D2009. Однако кодовая точка теперь может состоять из двух UnicodeChar(*) и Copy() может возвратить половину её.

UTF-8 в этом имеет преимущества. Поскольку мультибайтовые кодовые точки здесь встречаются постоянно, код, работающий со строками UTF-8 обычно корректен.

См. раздел ниже: Работа со строками и символами UTF8 в коде насчет примеров использования UTF-8 и того, как создавать код, правильно работающий с обеими кодировками.

(*)

  • Наименования "UnicodeString" и "UnicodeChar" для типов UTF-16 были весьма неудачным выбором со стороны Borland.
  • Кодовая точка Unicode - "реальное" определение символа в Unicode, которое может быть закодировано разными способами, и ее длина зависит от кодировки.
  • Символ Unicode может состоять как из одной кодовой точки, так и являться декомпозиционным символом нескольких кодовых точек. Да, это сложно...

Совместимость с LCL в Lazarus 1.x

Большинство приложений Lazarus LCL продолжат работать без изменений. Однако, поддержка Unicode становится проще, что является поводом подчистить код. Код, читающий из потоков или записывающий данные в потоки, файлы или базы данных в кодировках, не являющихся UTF-8, будет работать некорректно и потребует изменений. (Ниже см. примеры). Явное использование функций преобразования необходимо только при вызове Ansi-версий функций Windows API. Во всех других случаях FPC позаботится об автоматическом преобразовании кодировок. Функции-заглушки, тем не менее, предоставлены для возможности откомпилировать старый код.

  • UTF8Decode, UTF8Encode — В большинстве случаев все вызовы можно удалить.
  • UTF8ToSys, SysToUTF8, UTF8ToAnsi, AnsiToUTF8 - — В большинстве случаев все вызовы можно удалить.

Файловые функции в RTL теперь самостоятельно заботятся о кодировке имён файлов. Все (?) функции, относящиеся к именам файлов, содержащие в названии ...UTF8() могут быть заменены совместимыми с Delphi функциями без суффикса UTF8. Например, FileExistsUTF8 может быть заменено на FileExists.

Большинство строковых функций UTF8...() могут быть заменены на совместимые с Delphi функции Ansi...(). Функции UTF8...() из LazUTF8 регистрируются в качестве функций обратного вызова для функций Ansi...() из SysUtils.

UTF-8 работает также в программах без графического интерфейса. Для этого требуется зависимость от LazUtils и размещение юнита LazUTF8 в секции uses головного файла программы.

Чтение текстового файла со строками в кодовой странице Windows

Методика несовместима с прежним кодом Lazarus. Практически, вы должны вставить код, работающий с системной кодовой страницей и конвертировать данные в UTF-8 сразу, как только представится такая возможность.

Можно установить правильную кодовую страницу для строки следующим образом:

 var
   StrIn, StrOut: String;
 ...
 SetCodePage(RawByteString(StrIn), 1251, false);  // 1251 для русской Windows !! (или Windows.GetACP())
 ...

или использовать RawByteString и делать явное преобразование:

 uses ... , LConvEncoding;
 ...
 var
   StrIn: RawByteString;
   StrOut: String;
 ...
 StrOut := CP1252ToUTF8(StrIn,true);

ToDo ...

Код, в сильнейшей степени зависящий от кодовой страницы Windows

Код некоторых программ так тесно зависит от сиcтемной кодовой страницы, что использование нового режима RTL UTF-8 выглядит непрактичным. Здесь есть два выбора :

  • Продолжить использование Lazarus с FPC 2.6.4. Это хорошее решение для кода, находящегося в режиме поддержки. Lazarus пока способен компилироваться FPC 2.6.4 и старые функции UTF8...() на месте.
  • Использовать FPC 3.0 при отключенном новом режиме UTF-8, путем определения директивы DisableUTF8RTL. Это может повлечь некоторые весьма нехорошие проблемы, о которых написано здесь: Lazarus with FPC3.0 without UTF-8 mode.

Функции работы с кодовыми точками

В LazUtils появятся специальные функции для операций с кодовыми точками. Они будут не только использовать старые функции UTF8...() из LCL, но и будут иметь алиасы на функции, дающие возможность использовать кодировки, обычные как для Delphi, так и для FPC {$mode DelphiUnicode}.

  • CodePointCopy() - похоже на UTF8Copy()
  • CodePointLength() - похоже на UTF8Length()
  • CodePointPos() - похоже на UTF8Pos()
  • CodePointToWinCP()
  • WinCPToCodePoint()
  • CodePointByteCount() - похоже на UTF8CharacterLength()

Интересен вопрос, как CodePointCopy, CodePointLength и CodePointPos должны быть реализованы в Delphi, который не обеспечивает таких функции для UTF-16. (Или всё таки обеспечивает?) Практически весь код, написанный на Delphi использует обычные Copy, Length и Pos вместо функций, корректно работающих с кодовыми точками.

Работа со строками и символами UTF8 в коде

Детали см. в UTF8_strings_and_characters.

Строковые литералы

Исходники должны быть записаны в кодировке UTF-8. Lazarus создаёт такие файлы по умолчанию. Вы можете изменить кодировку импортируемых файлов кликом правой кнопкой мыши в окне редактора исходного кода и последовательным выбором пунктов всплывающего меню File Settings (Настройки файла) / Encoding (Кодировка).

Директива компиляции {$codepage utf8} / -FcUTF8 в большинстве случаев не требуется. Это выглядит нелогичным, потому что сам смысл этого флага в правильной интерпретации литералов, введенных в текст исходника в кодировке UTF-8. Однако, новый режим работы RTL во время исполнения установит UTF-8 кодировкой по умолчанию, и все строковые константы, не имеющие маркировки кодовой страницы, при соответствующих присвоениях будут проинтерпретированы правильно.

Примеры:

  • Литералы AnsiString/String работают как с директивой {$codepage utf8} / -FcUTF8 , так и без неё.
const s: string = 'äй';
  • Литералы ShortString работоспособны только без указания директивы {$codepage utf8} / -FcUTF8. Вы можете сделать следующее:
unit unit1;
{$Mode ObjFPC}{$H+}
{$modeswitch systemcodepage} // прекращает действие директивы -FcUTF8
interface
const s: string[15] = 'äй';
end.

В качестве альтернативы, возможно использовать строки shortstring с включенной директивой $codepage путем прямого присвоения кодов символов:

unit unit1;
{$Mode ObjFPC}{$H+}
{$codepage utf8}
interface
const s: String[15] = #$C3#$A4; // ä
end.
  • WideString/UnicodeString/UTF8String работают только с {$codepage utf8} / -FcUTF8.
unit unit1;
{$Mode ObjFPC}{$H+}
{$codepage utf8}
interface
const ws: WideString = 'äй';
end.

Запись в консоль операторами write

Некоторые консольные программы используют символы псевдографики, такие как '╩'. Например, в кодовой странице CP437 этот символ будет одним байтом с кодом #202 и он чаще всего используется следующим образом:

write('╩');

Когда вы конвертируете такой исходник в UTF-8, например использованием всплывающего меню редактора Lazarus File Settings / Encoding / UTF-8 и кликаньем в диалоговом окне кнопки "Change file on disk" (Изменить файл на диске), символ переводится в 3 байта (#226#149#169), так что литерал становится фактически строковым, а не символьным. Процедуры write и writeln переведут строку из кодировки UTF-8 в текущую кодировку консоли. Так что ваша консольная программа правильно выведет знак '╩' при работе под Windows с любой кодовой страницей (т. е. не только с кодировкой CP437) и будет работать под Linux и Mac OS X. Вы можете также использовать '╩' в строках LCL, скажем, вот так: Memo1.Lines.Add('╩');

Кодовые страницы FPC

Компилятор (FPC) поддерживает указание кодовой страницы, которое может быть записано через опцию командной строки -Fc (т.е. -Fcutf8) или эквивалентную директиву codepage (т.е. {$codepage utf8}). В этом случае, перед тем, как копировать байты, представляющие строковые константы в тексте вашей программы, компилятор будет интерпретировать все символьные двнные в соответствии с указанной кодовой страницей. Есть две вещи, которые не стоит упускать из виду:

  • На платформах Unix, менеджер широких строк должен быть обязательно включен добавлением юнита cwstring в перечень uses. Без него программа не сможет правильно преобразовывать строковые данные во время исполнения.

Менеджер широких строк добавляется по умолчанию в новом режиме UTF-8 RTL, однако это делает программу зависимой от libc и усложняет кросс-компиляцию.

  • Компилятор преобразует все строковые константы, содержащие символы, не относящиеся к ASCII, в константы типа widestring. Затем они автоматически преобразуются обратно в ansistring (либо во время компиляции, либо во время исполнения), но это может привести к искажениям, если вы попытаетесь смешать в одной строковой константе символы, напечатанные в тексте исходника и символы, заданные числовым представлением:

Например:

program project1;
{$codepage utf8}
{$mode objfpc}{$H+}
{$ifdef unix}
uses cwstring;
{$endif}
var
  a,b,c: string;
begin
  a:='ä';
  b:='='#$C3#$A4; // #$C3#$A4 - закодированный в UTF-8 символ ä
  c:='ä='#$C3#$A4; // после не относящегося к  ascii 'ä' компилятор интерпретирует #$C3 в качестве отдельного widechar.
  writeln(a,b); // выводит ä=ä
  writeln(c);   // выводит ä=ä
end.

После компиляции и исполнения программа выведет:

ä=ä
ä=ä

Причина в том, что после того как в строке обнаружен символ ä, как упоминалось выше, оставшаяся часть строковой константы, присваиваемой переменной 'c', будет обработана как widestring. В результате #$C3 и #$A4 интерпретируются как widechar(#$C3) и widechar(#$A4), вместо прямой трансляции в байтовое представление.

Открытые вопросы

  • Символьные переменные в TFormatSettings (баг 27086): например: ThousandSeparator, DecimalSeparator, DateSeparator, TimeSeparator, ListSeparator. Эти поля должны быть заменены на строки для корректной поддержки UTF-8. Например, под Linux с установками LC_NUMERIC=ru_RU.utf8 разделитель тысяч состоит из двух байт nbsp/160.
    • Обход проблемы: использовать только одиночные символы пробелов вместо того, что должно быть на самом деле, как это сделано в патче на баг 27099


Вызовы функций WinAPI в библиотеках FPC

  • Unit registry, TRegistry - этот unit использует Ansi функции Windows API и поэтому вам придётся пользоваться UTF8ToWinCP, WinCPToUTF8. Раньше для этого требовался вызов UTF8ToSys.
  • Все вызовы Windows функций с Ansi API в библиотеках FPC должны быть заменены версией W-API. Это в любом случае будет сделано для будущей поддержки UTF-16, поэтому никакого конфликта интересов нет. (прим. перев. - однако, эти действия означают декларированный отказ от дальнейшей поддержки версий Windows с неполной поддержкой Unicode - Windows 98, 95 и более ранних, а также некоторых мобильных и встроенных)
  • TProcess - под Windows TProcess FPC 3.0 поддерживает только системную кодовую страницу. Необходимо либо использовать TProcessUTF8 из unit utf8process или патчить FPC, см. баг 29136

ToDo: Перечислить все относящиеся к багтрекеру FPC проблемы и патчи, которые могут эти проблемы решить.

Будущее

Целью проекта FPC является создание решения, базирующегося на Delphi-совместимом UnicodeString (UTF-16), но пока мы к этому не готовы. Потребуется длительное время для такой реализации.

Реализацию LCL на базе UTF-8 в её имеющемся виде необходимо рассматривать как временное решение. В будущем, когда в FPC будет полная поддержка UnicodeString как в RTL, так и в FCL, проект Lazarus обеспечит решения для LCL, использующее эти возможности. В то же время целью является и сохранение поддержки UTF-8, несмотря на то, что это может потребовать изменения в строковых типах или чего-то ещё. Деталей пока не знает никто. Мы обязательно сообщим вам о них, когда станет известно...

В сущности, LCL скорее всего придётся в будущем разделиться на две версии - одну для UTF-8, и другую для UTF-16.

FAQ

Что насчёт режима DelphiUnicode?

Директива {$mode delphiunicode} была добавлена в FPC 2.7.1 и работает как комбинация {$Mode Delphi} совместно с {$ModeSwitch UnicodeStrings}. См. следующий вопрос относительно ModeSwitch UnicodeStrings.

Что насчёт ModeSwitch UnicodeStrings?

Директива {$ModeSwitch UnicodeStrings} была добавлена в FPC 2.7.1 и определяет тип "String" синонимом "UnicodeString" (UTF-16), "Char" синонимом "WideChar", "PChar" синонимом "PWideChar" и т.д. Директива действует только на unit, в котором она объявлена. Другие юниты, имеющие в списке uses юнит с это директивой, имеют собственное определение типа "String". Многие строки и типы RTL (такие как TStringList) используют 8-битовые строки, что требует преобразований из/в UnicodeString, и необходимый для этого код автоматически добавляется компилятором. LCL использует строки UTF-8. Рекомендуется пользоваться исходниками в кодировке UTF-8 и компилировать с ключем "-FcUTF8".

Почему в Lazarus не используется строковый тип UTF8String?

Короткий ответ: потому что FCL не использует этот тип данных.

Развёрнутый ответ: UTF8String определяется в unit system как

UTF8String          = type AnsiString(CP_UTF8);

Компилятор подразумевает, что такая строка всегда имеет кодировку UTF-8 (CP_UTF8), которая является мультибайтовой кодировкой (т.е. 1-4 байта на кодовую точку). Обратите внимание на то, что оператор [] на данных такого типа обеспечивает доступность байтов, а не символов и не кодовых точек. То же верно и для UnicodeString, только здесь доступны слова (тип word) вместо байтов. С другой стороны, константам String во время компиляции присваивается маркировка кодовой страницы DefaultSystemCodePage (CP_ACP). DefaultSystemCodePage определяется во время исполнения, поэтому компилятор консервативно подразумевает, что String и UTF8String имеют различные кодировки. Когда вы присваиваете или комбинируете String и UTF8String, компилятор вставляет код преобразования. То же для ShortString и UTF8String.

Lazarus использует FCL, повсеместно построенный на применении String, поэтому использование UTF8String добавит дополнительные преобразования. Если DefaultSystemCodePage не в кодировке UTF-8, вы потеряете символы. Если же она в кодировке UTF-8, то и в этом случае нет никаких преимуществ в использовании отдельного типа UTF8String.

UTF8String станет полезной, когда FCL начнет работать со строками UTF-16.