Кодировки и Unicode в FPC |
26.10.2005 Иван Шихалев |
Одним из насущнейших вопросов для всякого русскоязычного программиста является вопрос кодировок. Так уж исторически сложилось, что компьютеры пришли к нам из англопишуших стран, чей алфавит насчитывает всего лишь 26 букв, а мы, спасибо Кириллу с Мефодием, пользуемся несколько другими. Впрочем, Кирилла и Мефодия винить не за что — они это не со зла, да и родной им греческий алфавит тоже далек от латиницы.
Как бы то ни было, мы в этом вопросе не одиноки. Более того, народам с иероглифической письменностью — еще хуже. Всемирное братание, когда все будут говорить на одном языке и писать на одном алфавите пока не предвидится, а посему возник некоторый стандарт кодирования символов, неэкономный для отдельно взятого языка, зато позволяющий работать сразу со многими.
Было бы заманчиво назвать Unicode “универсальной кодировкой”, но, к сожалению, такой единой кодировки не существует. Стандарт определяет порядковый номер каждого символа, однако их очень уж много. Отводить под каждый символ 4 байта и более как-то никому не хочется. Поэтому используется несколько способов кодирования: в одних случаях используется ограниченное подмножество символов, кодируемое одним или двумя байтами; в других — количество байт на символ зависит от самого символа.
Среди ограниченных вариантов главное место занимают однобайтные, которые, впрочем, к Unicode никакого отношения не имеют… Для кириллицы таких кодировок, к сожалению, много: KOI8-r, используемая в *nix-системах и ставшая стандартом de facto для электронной почты; кодировка Windows 1251, используемая в MS Windows, cp866 — альтернативная кодировка DOS… И это только наиболее распространенные. Что особенно «приятно», так это то, что кодировка, утвержденная Международной Организацией по Стандартизации — ISO, в силу ряда причин неудобна и практически никем не используется. Среди двухбайтных следует выделить UCS, которая использовалась в операционных системах Windows до версии 2000/XP.
Кодировки с переменным числом байт на символ тоже бывают разные — различается «минимальный символ». Для нас наиболее интересными будут кодировки UTF-8, где минимум — один байт, и UTF-16, где, соответственно, два. UTF-16 используется в Windows 2000/XP (и, скорее всего, будет использоваться в последующих версиях), а UTF-8 широко распространена на *nix'овых платформах, но любим мы ее не только за это…
Дело в том, что FPC, как и большинство компиляторов и подобных им программ, воспринимает тексты только в однобайтной кодировке, причем в качестве значимых используются только символы из первой половины кодовой таблицы. В UTF-8 эти символы кодируются точно также. Иными словами, исходный код в UTF-8 компилятор прекрасно воспримет, какими бы ни были символы в строках и комментариях.
Что касается UTF-16, следует заметить, что все буквы европейских языков и знаки препинания кодируются одним двухбайтным символом. Таким образом, для большинства наших задач работа с ней и с UCS-кодировкой отличаться не будут.
Чтобы не останавливаться подробно на вопросе Unicode в целом, рекомендую статью «Unicode» в русской «Википедии» — там довольно ясно и подробно изложены основные сведения.
В Free Pascal могут использоваться два типа символов, из которых
составляются строки: одно- и двух-байтные. Для первых есть тип
Char
(он же — AnsiChar
), для вторых —
WideChar
. И соответственно: string
,
AnsiString
, WideString
, PChar
и PWideChar
. Таким образом, мы можем использовать одно- и
двух-байтные кодировки, причем как ограниченные, так и с переменным
объемом символа, поскольку нулевой символ, имеющий специальное значение,
везде одинаков.
Free Pascal поддерживает автоматическое преобразование Ansi и Wide-строк друг в друга. Тем не менее, кто пытался этим воспользоваться, испытал, наверное, жестокое разочарование — преобразуются только символы из первой части таблицы, ради которых переходить к двухбайтным кодировкам было б странно. Но не надо думать, что эта часть разработчиками FPC недоделана. Дело в том, что кодировки у всех разные, и задать такое преобразование «для всех» невозможно. В принципе, можно было б привязаться к текущим настройкам локализации, однако это добавляет платформо-зависимый код, и резко ухудшает переносимость программ. Вместо этого, определение перекодировки отдано на откуп программисту.
Управление перекодировкой зависит структуры следующего типа:
type TWideStringManager = record Wide2AnsiMoveProc : procedure (Source : PWideChar; var Dest : AnsiString; Len : SizeInt); Ansi2WideMoveProc : procedure (Source : PChar; var Dest : WideString; Len : SizeInt); UpperWideStringProc : function (const S : WideString) : WideString; LowerWideStringProc : function (const S : WideString) : WideString; CompareWideStringProc : function (const S1, S2 : WideString) : PtrInt; CompareTextWideStringProc : function (const S1, S2 : WideString) : PtrInt; CharLengthPCharProc : function (const Str : PChar) : PtrInt; UpperAnsiStringProc : function (const S : AnsiString) : AnsiString; LowerAnsiStringProc : function (const S : AnsiString) : AnsiString; CompareStrAnsiStringProc : function (const S1, S2 : AnsiString) : PtrInt; CompareTextAnsiStringProc : function (const S1, S2 : AnsiString) : PtrInt; StrCompAnsiStringProc : function (S1, S2 : PChar) : PtrInt; StrICompAnsiStringProc : function (S1, S2 : PChar) : PtrInt; StrLCompAnsiStringProc : function (S1, S2 : PChar; MaxLen : PtrUInt) : PtrInt; StrLICompAnsiStringProc : function (S1, S2 : PChar; MaxLen : PtrUInt) : PtrInt; StrLowerAnsiStringProc : function (Str : PChar) : PChar; StrUpperAnsiStringProc : function (Str : PChar) : PChar; end;
Впрочем, как видим, эта структура управляет не только преобразованием Ansi <-> Wide, но и другими действиями над строками, зависящми от кодировки — преобразованием регистра, порядком сортировки и т.д. Для управления используются следующие три процедуры:
procedure GetWideStringManager (var Manager : TWideStringManager); procedure SetWideStringManager (const New : TWideStringManager); procedure SetWideStringManager (const New : TWideStringManager; var Old: TWideStringManager);
Замечу, однако, что не все строковые функции модулей
System
и SysUtils
используют данную структуру.
Кроме того, для полноценной работы с UTF-8, например, ее недостаточно.
В целом, работа над RTL в этом направлении еще не закончена.
Теперь посмотрим, что нам может предложить модуль System
для работы с UTF-8.
type UTF8String = type AnsiString; function UnicodeToUtf8 ( Dest : PChar; Source : PWideChar; MaxBytes : SizeInt ) : SizeInt; function UnicodeToUtf8 ( Dest : PChar; MaxDestBytes : SizeUInt; Source : PWideChar; SourceChars : SizeUInt ) : SizeUInt; function Utf8ToUnicode ( Dest : PWideChar; Source : PChar; MaxChars : SizeInt ) : SizeInt; function Utf8ToUnicode ( Dest : PWideChar; MaxDestChars : SizeUInt; Source : PChar; SourceBytes : SizeUInt ) : SizeUInt; function UTF8Encode (const S : WideString) : UTF8String; function UTF8Decode (const S : UTF8String) : WideString; function AnsiToUtf8 (const S : AnsiString) : UTF8String; function Utf8ToAnsi (const S : UTF8String) : AnsiString;
Это то, что в разделе “interface
”. Если заглянуть в
“implementation
”, то увидим, что последние две функции
реализованы через умолчательное преобразование Ansi <-> Wide.
Таким образом для их использования требуется адекватно определить это
преобразование. Как — см. выше. В то же время преобразование UTF-8
<-> Wide определено практически полностью — включая 3-байтные
символы (кириллица находится в 2-байтных).
В целом на сегодняшний день работа с кодировками и Unicode в FPC вполне возможна. Далее мы еще поговорим, как проще всего добавить поддержку кириллицы. Заметим, что все вопросы, возникающие при локализации, решаются на уровне RTL, более того, их можно решить и не меняя стандартные модули — переопределение операторов и перегрузка функций могут производиться и в отдельных модулях (тут, правда, есть тонкости с текстовыми файлами). Управление строками на низком уровне, требующее поддежки компилятора полностью реализовано, за исключением, разве что совсем экзотических UTF-32 и UCS4 кодировок.
Шлифовка RTL, на мой взгляд, требуется в направлении большей стройности кода и ориентации на UTF-16 вместо UCS2 для WideString (то есть на использование переменной двухбайтной кодировки вместо ограниченной) — вдруг придется писать что-то для юго-восточной Азии… С другой стороны, строки с переменным числом байт на символ не позволяют уже обращаться с ними как с массивами символов и вообще значительно усложняют работу…
В стандартном наборе FPC наблюдается еще кое-что, что может облегчить
работу с различными кодировками и Unicode. Это модуль
CharSet
и утилита creumap.
Модуль CharSet
предлагает некое стандартное
представление таблиц перекодировки. Вообще-то, он выглядит недоделаным и
сам по себе малополезен, но в сочетании с программой
creumap
позволяет написать перекодирующие функции гораздо
быстрее, чем с нуля.
Утилита формирует модули с таблицами перекодировки для
CharSet
из текстовых файлов специального вида. Что было бы
тоже не особо полезно, если бы набор таких файлов не был включен в
состав исходников FPC. Кстати, саму утилиту тоже придется компилировать
из исходников — в бинарном дистрибутиве ее нет.
Поскольку без исходников все равно не обойтись, я не буду описывать
модуль CharSet
— его интерфейс умещается на одном экране —
разобраться просто. Вместо этого замечу, что исходник
CharSet
находится в файле
“./rtl/inc/charset.pp
”, creumap
—
“./utils/creumap.pp
”, а файлы кодировок — в каталоге
“./rtl/ucmaps/
”, считая от корня исходников FPC. В
частности, там наличествуют файлы для кодировок кириллицы: ISO-8859-5,
cp855 (DOS-основная, практически не используемая), cp866
(DOS-альтернативная), cp1251 (Windows). KOI8-r, к сожалению, нет.
Таблицы перекодировки модуля CharSet
устроены так, что
нужный символ Unicode по ANSI выбирается сразу — как элемент таблицы по
индексу, тогда как для обратного преобразования требуется перебор
элементов, что не есть хорошо в случае перекодировки больших объемов
текста.
В целом картина типичная для Free Pascal: компилятор свое дело делает, RTL базовую функциональность реализует, хотя напильник бы не помешал, а для практического применения нужно немного поработать ручками и ясно представлять, что и как происходит. Надеюсь данная статья хотя бы немного поспособствует последнему — в документации, к сожалению, почти ничего на эту тему нет.