понедельник, 28 марта 2016 г.

Использование UTF-8 в исходных кодах


Некоторые время назад возникла задача проверить совместимость компиляторов с исходными файлами в представлении UTF-8 с маркером и без него. Маркер BOM был придуман для индикации порядка байт в машинных словах. Несмотря на то, что UTF-8 — это байтовый поток и такой маркер не имеет большого смысла в его первоначальном понимании, многие утилиты корректно работают только при наличии маркера в файле. Для обозначения UTF-8 в начало файла вставляется последовательность EF16, BB16, BF16. Файлы в кодировке UTF-8 без маркера неотличимы от ASCII файлов, если используются только 7-битные символы.

В результате эксперимента исследовались файлы в вариантах Unicode (UTF-8 with signature) – Codepage 65001 и Unicode (UTF-8 without signature) – Codepage 65001 для Visual Studio 2013, Visual Studio 2008 и Visual Studio 2014 CTP2. Для полноты сравнения в системе Linux была проведена аналогичная проверка с компилятором GNU C++ 4.7.2. В качестве редактора в Linux использовался vi с опциями set [no]bomb для получения файлов с маркером и без него.

В качестве входного файла использовался следующий исходный код:
#include <iostream>
#include <iomanip>
#include <string>
int main()
{
     using namespace std;
     wstring wa = L"grüßen";
     string a = "grüßen";
     cout << hex << uppercase << setfill('0');
     cout << "char: ";
     const size_t asize = a.size() * sizeof(string::value_type);
     const char* abuf = reinterpret_cast<const char*>(&a[0]);
     for (size_t i = 0; i < asize; ++i)
     {
          cout << setw(2) << (static_cast<unsigned>(abuf[i]) & 0xFF) << ' ';
     }
     cout << endl;
     cout << "wchar_t: ";
     const size_t wasize = wa.size() * sizeof(wstring::value_type);
     const char* wabuf = reinterpret_cast<const char*>(&wa[0]);
     for (size_t i = 0; i < wasize; ++i)
     {
          cout << setw(2) << (static_cast<unsigned>(wabuf[i]) & 0xFF) << ' ';
     }
     cout << endl;
     return 0;
}
В коде используется текст для инициализации multi-byte последовательности (string a) и для инициализации последовательности wide-characters (wstring wa). Надо отметить, что размер wchar_t сильно зависит от платформы. На Linux размер wchar_t составляет четыре байта, тогда как на Windows его размер равен двум байтам.

В данном примере проверяется корректность инициализации строковых констант из исходных кодов UTF-8. Результаты показаны в таблице ниже. Все некорректные результаты выделены красным цветом.
Описание окруженияРезультат
Reference UTF-8 code
Reference UTF-16LE
char: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
g++ (Debian 4.7.2-5) 4.7.2 w BOMchar: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 00 00 72 00 00 00 FC 00 00 00 DF 00 00 00 65 00 00 00 6E 00 00 00
g++ (Debian 4.7.2-5) 4.7.2 w/o BOMchar: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 00 00 72 00 00 00 FC 00 00 00 DF 00 00 00 65 00 00 00 6E 00 00 00
Visual Studio 2008 w BOMchar: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566)
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
Visual Studio 2008 w/o BOMchar: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00
Visual Studio 2013 w BOMchar: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566)
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
Visual Studio 2013 w/o BOMchar: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00
Visual Studio 2014 CTP 2 w BOMchar: 67 72 3F 3F 65 6E (компилятор выдает предупреждение C4566)
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
Visual Studio 2014 CTP 2 w/o BOMchar: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00
Visual Studio 2010 SP1 и выше
without BOM
#pragma execution_character_set("utf-8")
char: 67 72 D0 93 D1 98 D0 93 D1 9F 65 6E
wchar_t: 67 00 72 00 13 04 58 04 13 04 5F 04 65 00 6E 00
Visual Studio 2010 SP1 и выше
with BOM
#pragma execution_character_set("utf-8")
char: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
Visual Studio 2015 Update 2
with BOM
u8"grüßen"
L"grüßen"
char u8: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00
Visual Studio 2015 Update 2
without BOM
u8"grüßen"
L"grüßen"
char u8: 67 72 C3 BC C3 9F 65 6E
wchar_t: 67 00 72 00 FC 00 DF 00 65 00 6E 00

Из результатов видно, что компилятор Visual Studio всегда пытается преобразовать файл в текущую системную кодировку, а потом уже его компилирует. Самый надежный способ этого избежать — это инициализировать константы с помощью численных значений, но это не очень удобно.

Стоит отметить, что начиная с Visual Studio 2010 SP1 поддерживается директива execution_character_set, которая на практике заставляет компилятор воспринимать исходный код как UTF-8 и не преобразовывать его (последний пункт в таблице). Также существует хот-фикс, который вводит поддержку директивы в Visual Studio 2008 SP1. И еще, в Visual Studio 2015 Update 2 CTP ввели новый параметр сборки: /source-charset. Он позволяет задать кодировку для всех файлов, в которых ее не удалось определить автоматически, что означает поддержку файлов без маркера BOM.

Надеюсь, кому-то это исследование окажется полезным. Я придерживаюсь мнения, что локализованные строковые константы все-таки лучше хранить в отдельных файлах с ресурсами, чтобы не зависеть от версии и возможностей компилятора на конкретной платформе.

Ссылки по теме:
  1. sizeof char и другие сложности с типами
  2. Compiler Warning (level 1) C4566
  3. Visual Studio & UTF8
  4. New Options for Managing Character Sets in the Microsoft C/C++ Compiler

Комментировать в ВКонтакте