Разбираемся с UTF-8 в GNAT

Программист впервые сталкивающийся с разработкой на Аде в GNAT, возможно пойдёт по нескольким граблям, пока разберётся, как работает UTF-8 в этом инструментарии. Хотя в стандарте языка Ада и написано, что компилятор обязан понимать тексты в UTF-8 кодировке, это не значит, что компилятор должен делать это «по умолчанию». Компилятор GNAT не нарушает стандарт, но и по умолчанию кодировку UTF-8 не воспринимает. Это же касается и остальных инструментов GNAT.

Настраиваем GNAT Studio

Поставив компилятор и среду разработки GNAT Studio, разработчик напишет первую программу:

with Ada.Text_IO;

procedure Main is
begin
   Ada.Text_IO.Put_Line ("Привет");
end Main;

Но он не сможет ни собрать эту программу, ни даже сохранить. GNAT Studio выдаст ошибку:

This buffer contains UTF-8 characters which could not be translated to ISO-8859-1.

Some data may be missing in the saved file: check the Locations View.

You may change the character set of this file through the "Properties..." contextual menu.

А в Locations View выдаст

Error converting from UTF8 to ISO-8859-1
    /tmp/utf8/src/main.adb
        5:1 Недопустимая последовательность байтов во входных преобразуемых данных

Смена кодировки через контекстное меню в «Properties...» сработает только для этого отдельного файла, а для следующего будет тоже самое.

Вместо этого лучше сменить кодировку по умолчанию. Зайдя в настройки «/Edit/Preferences...» в категории «General» измените «Character set» на «Unicode UTF-8». Пересоздайте файл «main.adb» и он успешно сохранится.

Настраиваем компилятор

Теперь программу можно собрать и она даже отработает, как ожидалось (только, если у вас консоль в UTF-8; в Windows вы сожете переключить cmd.exe консоль в UTF-8 используя команду chcp 65001), но с точки зрения компилятора строка «Привет» будет иметь длину 12 символов. Дело в том, что по стандарту (ARM 3.5.2 2/3) в набор символов Character входит только 256 значений из множества Latin-1. В нём нет кириллицы.

Компилятор не знает, что в IDE вы задали кодировку по умолчанию UTF-8. Он по прежнему использует свою кодировку по умолчанию:

Ada source programs are represented in standard text files, using Latin-1 coding.

Он видит эту программу как

with Ada.Text_IO;

procedure Main is
begin
   Ada.Text_IO.Put_Line ("Ð_Ñ_ивеÑ_");
end Main;

Где символом _ отмечены вообще непечатные символы набора Latin-1.

Указать компилятору кодировку входных текстов можно передав ключ -gnatW8. В списке опций (gnatmake --help) она описана так

  -gnatW?   Wide character encoding method (?=h/u/s/e/8/b)

Проще всего указать эту опцию в свойствах проекта выбрав меню «/Edit/Project Properties...», закладку «/Build/Switches/Ada» и вписать -gnatW8 в строку опций внизу формы.

При этом проектный файл default.gpr примет вид:

project Default is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Main use ("main.adb");

   package Compiler is
      for Switches ("ada") use ("-gnatW8");
   end Compiler;

end Default;

Теперь компилятор увидит кириллицу и откажется строить программу:

Builder results
  /tmp/utf8/src/main.adb
    5:27 error: literal out of range of type Standard.Character

И будет прав. Нам нужен другой тип строки. Да, в Аде есть несколько типов строк. Их даже слишком много. И не один не годится для всех случаев реальной жизни. По строчным типам языка Ада можно проследить как развивались кодировки строк. До первой версии языка в Character помещалось только 127 символов набора ASCII. Но это быстро исправили расширив Character до 256 значений Latin-1. В следующей версии стандарта, это приблизительно время возникновения Java, где символ имел размер 16 бит, ввели Wide_Character, который вмещал 65536 символов и казалось, этого хватит всем. Но появился Unicode с репертуаром в 1,114,112 «code points». Тогда ввели Wide_Wide_Character с размером 32 бита и набором значений в 2 миллиарда символов. Замете, «старые» типы Character/String, Wide_Character/Wide_String никто не спешил удалять. Более того String широко используется в стандартной библиотеке! Например в именах файлов ввода‐вывода, в Ada.Environments и пр. Вот, где можно развернуть работу по введению костылей. Но я пока не об этом.

Заменим String на Wide_String? Легко! Но нам понадобится другой пакет вместо Text_IO:

with Ada.Wide_Text_IO;

procedure Main is
begin
   Ada.Wide_Text_IO.Put_Line ("Привет");
end Main;

Или, для большей наглядности:

with Ada.Wide_Text_IO;

procedure Main is
   Hello : constant Wide_String := "Привет";
begin
   Ada.Wide_Text_IO.Put_Line (Hello);
end Main;

О, работает!

Имена в кириллице

Ещё один ключ компилятора -gnatiw меняет взгляд компилятора на возможные имена в программе. В руководстве GNAT пишут:

Normally GNAT recognizes the Latin-1 character set in source program identifiers, as described in the Ada Reference Manual. This switch causes GNAT to recognize alternate character sets in identifiers.

Он нам нагло врёт, потому, что в ARM лексема identifier определена в терминах Unicode а не в символах Latin-1. Т.е. использование, например, кириллицы в идентификаторах разрешено стандартом. Чтобы заставить компилятор следовать стандарту нужно указать ему ключ -gnatiw (но ключ -gnatW8 нужен тоже):

with Ada.Wide_Text_IO;

procedure Main is
   Привет : constant Wide_String := "Привет";
begin
   Ada.Wide_Text_IO.Put_Line (Привет);
end Main;

Использование таких имен не особо поощряется, но я нашел их очень удобными, когда пытался написать иситему типов для анализа морфологии русского языка. Познания в английском у меня заканчиваются на терминах «существительное» и глаголг», поэтому следующий текст для меня более приемлем чем в латинице:

package Ru is
   type Часть_Речи is
     (Существительное,  --          Ч,П,
      Глагол,           --  Л,В,Н,Р,Ч,
      Прилагательное,   --        Р,Ч,П
      Числительное,     --            П
      Наречие,
      Предлог,
      Союз,
      Частица,
      Междометие
     );

   type Лицо is ('1', '2', '3');
   type Время is (Прошедшее, Настоящее, Будущее);
   type Hаклонение is (Обычное111, Повелительное, Сослогательное);
   type Число is (Единственное, Множественное);
   type Род is (Общий, Мужской, Женский, Средний);
   type Падеж is
     (Именительный,   --  кто? что?
      Родительный,    --  кого? чего? откуда?
      Дательный,      --  кому? чему?
      Винительный,    --  кого? что? куда?
      Творительный,   --  кем? чем?
      Предложный);    --  (о) ком? (о) чем? где?

Но для имён файлов старайтесь использовать только ASCII. Там всё сложно.

Когда -gnatW8 не нужен

Не все проекты разрешают в исходном коде иметь произвольные UTF-8 символы. Некоторые ограничиваются ASCII подмножеством. В этом случае вам нет нужды указывать -gnatW8, так ведь? Уберём -gnatW8 и посмотрим, что будет. Слово «Привет» мы будем «вычислять":

with Ada.Wide_Text_IO;

procedure Main is
   Hello : constant Wide_String :=
     Wide_Character'Val (1055) &
     Wide_Character'Val (1088) &
     Wide_Character'Val (1080) &
     Wide_Character'Val (1074) &
     Wide_Character'Val (1077) &
     Wide_Character'Val (1090);
begin
   Ada.Wide_Text_IO.Put_Line (Hello);
end Main;

Резултат, ну «необычен» :

["041F"]["0440"]["0438"]["0432"]["0435"]["0442"]

Что это? Это так называемый Brackets encoding изобретённый авторами GNAT на заре становления Wide_Character/Wide_String. Давайте заглянем в пакет Ada.Numerics:

До недавних пор, он выглядел так

package Ada.Numerics is
   pragma Pure;

   Argument_Error : exception;

   Pi : constant :=
          3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;

   ["03C0"] : constant := Pi;
   --  This is the Greek letter Pi (for Ada 2005 AI-388). Note that it is
   --  conforming to have this constant present even in Ada 95 mode, as there
   --  is no way for a normal mode Ada 95 program to reference this identifier.

В ARM он описан как:

package Ada.Numerics is
   pragma Pure(Numerics);
   Argument_Error : exception;
   Pi : constant :=
          3.14159_26535_89793_23846_26433_83279_50288_41971_69399_37511;
   π  : constant := Pi;
   e  : constant :=
          2.71828_18284_59045_23536_02874_71352_66249_77572_47093_69996;
end Ada.Numerics;

В последних версиях компилятора эту константу вообще удалили!

   --  ???This is removed for now, because nobody uses it, and it causes
   --  trouble for tools other than the compiler. If people want to use the
   --  Greek letter in their programs, they can easily define it themselves.

Заставить компилятор использовать UTF-8 можно и в этом случае, передав опцию -W8 в стадию gnatbind (В GNAT Studio закладка «/Build/Switches/Binder"):

project Default is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Main use ("main.adb");

   package Binder is
      for Switches ("ada") use ("-W8");
   end Binder;

end Default;

Другим способом это можно сделать при открытии файла в параметре Form => «WCEM=8»

with Ada.Wide_Text_IO;

procedure Main is
   Hello : constant Wide_String :=
     Wide_Character'Val (1055) &
     Wide_Character'Val (1088) &
     Wide_Character'Val (1080) &
     Wide_Character'Val (1074) &
     Wide_Character'Val (1077) &
     Wide_Character'Val (1090);
   Output : Ada.Wide_Text_IO.File_Type;
begin
   Ada.Wide_Text_IO.Create (Output, Name => "aaa.txt", Form => "WCEM=8");
   Ada.Wide_Text_IO.Put_Line (Output, Hello);
end Main;

Когда вы используете -gnatW8 ключ gnatbind -W8 используется автоматически. Можно ставить их оба, это не повредит.

Другие программы GNAT

Используя другие программы из GNAT поищите у них флаги с кодировками:

  • gnatpp --wide-character-encoding=8
  • gnatstub --wide-character-encoding=8

Увы даже Ada Language Server (часть расширения для Ады под VS Code) использует по умолчанию iso-8859-1. Исправте это в настройках через параметр

    'defaultCharset': 'UTF-8'

Переменные окружения

Не ждите, что ваша программа будет сама обращать внимание на установки локали (типа LANG=), но берегитесь переменных окружения GNAT_CCS_ENCODING, GNAT_CODE_PAGE на Windows.

Библиотеки Matreshka/VSS

Некоторое время стандарт Ады был однозначен в том, что в Character/String могут находится только символы Latin-1. Но в какой‐то момент он дрогнул под натиском «любителей простых решений» и в нём появились функции перобразования Wide_String/Wide_Wide_String в UTF-8, которые для представления UTF-8 используют не массив байт, а тип String.

Авторы GNAT пользуются этим в полный рост, позволяю, например, передавать в Ada.Text_IO.Create имя файла в UTF-8 кодировке через параметр типа String.

Введение типа Wide_Wide_String на самом деле не решает проблем использования Unicode, т.к. этот стандарт манипулирует не «символами», а их комбинациями. Появляются варианты, когда несколько «code points» образуют едиственный глиф при печати/отображении. Пользователю за частую удобнее работать в таких понятиях. Это логично для указания положения в тексте «строка/столбец». Wide_Wide_String тут не спасает.

Библиотека VSS вводит свой тип для Unicode строки с для рабты удобными методами. В ней можно найти границы символов, графем‐кластеров, их смещение в UTF-8/UTF-16 кодировке и пр.

Библиотека Matreshka позволяет оперировать строками в терминах Unicode «code points», имеет набор перекодировщиков в разные системы кодирования (типа Windows-1251, KOI8-R), поддержку JSON, XML, баз данных, регулярных выражений, движок XML шалонов и пр.

Выводы

  1. Всегда ставте ключ компилятора —gnatW8, не помешает для gnatbind иметь ключ -W8.
  2. Настраивайте среду разработки сразу на работу в UTF-8.
  3. Посмотрите библиотеки VSS и Matreshka, с ними работа в Unicode не доставляет боли.

-- Максим