C: Unicode и UTF8 с использованием wchar_t, wprintf и стандарта C99

THE HOLY BIBLE - King James Version - БИБЛИЯ в Синодальном переводе
"Нас Атакуют!" Изобличи козни лукавого, запрети диаволу

C: Unicode и UTF8 с использованием wchar_t, wprintf и стандарта C99

Программируя на С, каждый неанглоязычный программист рано или поздно сталкивается с неожиданной и досадной трудностью вывода русского текста в наборе символов Юникод (Unicode) и в кодировке UTF8. Чтение стандарта С99 и описания библиотеки libc навевает уныние, поиск в Интернете выдает массу полезных ссылок об использовании wchar_t, не приводя в то же время простых примеров работы с "wchar.h" и широкими функциями.

Эта заметка рассматривает два самых простых способа вывода русского текста из консольных С-программ.

Прежде чем мы продолжим, я хотел бы привести строки из Евангелия:



........... == Первое соборное послание святого апостола Петра == .............. === Глава 2, Стих 24 === 24 Он грехи наши Сам вознес телом Своим на древо, дабы мы, избавившись от  грехов, жили для правды: ранами Его вы исцелились.

Лично для вас благая весть - Единородный Сын Божий Иисус Христос любит вас, Он взошел на крест за ваши грехи, был распят и на третий день воскрес, сел одесную Бога и открыл нам дорогу в Царствие Небесное.

Сейчас, в это время года перед Пасхой, Православные христиане приносят Господу жертву поста и молитвы. Мы делаем это в воспоминание великого подвига Иисуса Христа, совершенного для нас и за нас. Мы вспоминаем со стыдом и раскаянием свои грехи, исповедуем их в молитве перед Богом и просим Его милости к нам, христианам. Каждый из нас молится за своих родных, друзей, соседей, коллег и всех людей, окружающих нас. Эта молитва обретает новую силу во время поста.

Прочитав эти строки, знайте - именно сейчас Православные всего мира молятся за вас, именно за вас, дорогой читатель. Мы молим Бога о вашем спасении, просим лично вас посмотреть на свою жизнь, открыть Библию, начать чтение Святого Писания и осознать как нуждается ваша душа в общении с Богом.

Покайтесь, примите Иисуса как вашего Спасителя, ибо наступают последние времена и время близко - стоит Судья у ворот.

Пожалуйста, в своих каждодневных трудах, какими бы занятыми вы себе ни казались - находите время для Бога, Его заповедей и Библии.

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


Вернемся к нашим техническим деталям.

Я предполагаю что уважаемый читатель владеет необходимыми знаниями и навыками использования С-компиляторов и знаком с языком, что позволит мне изложить самый простой подход к работе с русскими Юникод-символами в кодировке UTF8.

Простейший способ вывода

Предположим, что мы хотим всего лишь вывести некоторый текст из нашей программы, без необходимости его обработки. Сделать это можно очень просто, используя стандартные одно-байтные функции. Убедитесь что консоль системы установлена в UTF8 и текст вашей программы использует эту же кодировку.

u@x> locale
LANG=en_US.utf8
LC_CTYPE="en_US.utf8"
LC_NUMERIC="en_US.utf8"
LC_TIME="en_US.utf8"
LC_COLLATE="en_US.utf8"
LC_MONETARY="en_US.utf8"
LC_MESSAGES="en_US.utf8"
LC_PAPER="en_US.utf8"
LC_NAME="en_US.utf8"
LC_ADDRESS="en_US.utf8"
LC_TELEPHONE="en_US.utf8"
LC_MEASUREMENT="en_US.utf8"
LC_IDENTIFICATION="en_US.utf8"
LC_ALL=
u@x> file 1.c
1.c: UTF-8 Unicode C program text
u@x> cat 1.c
#include <stdio.h>
int main()
{
const char *msg = "Добро Пожаловать";
printf("%s\n", msg);
}
u@x> gcc 1.c
u@x> ./a.out
Добро Пожаловать
u@x>

Как видно, текст выведен правильно, несмотря на использование однобайтовых ASCII функций. Что же произошло? Фактически, мы "закодировали" нашу константу "Добро Пожаловать" не в 16 байт, а в 32 байта, и вывели их последовательно, один за другим. На экране, поток ввода-вывода stdout всего лишь преобразовал эти 32 байта в необходимые 15 двухбайтных Unicode символов (и 2 однобайтных символа), в соответствии с Unicode UTF8 encoding. Проверим это:

u@x> ./a.out
Добро Пожаловать
u@x> ./a.out | wc
1       2      32
u@x>
u@x> ./a.out | xxd
0000000: d094 d0be d0b1 d180 d0be 20d0 9fd0 bed0  .......... .....
0000010: b6d0 b0d0 bbd0 bed0 b2d0 b0d1 82d1 8c0a  ................
u@x> ./a.out | xxd -b
0000000: 11010000 10010100 11010000 10111110 11010000 10110001  ......
0000006: 11010001 10000000 11010000 10111110 00100000 11010000  .... .
000000c: 10011111 11010000 10111110 11010000 10110110 11010000  ......
0000012: 10110000 11010000 10111011 11010000 10111110 11010000  ......
0000018: 10110010 11010000 10110000 11010001 10000010 11010001  ......
000001e: 10001100 00001010                                      ..
u@x>

Заметьте выделенные жирным последовательности битов "110" и "10" в соседних байтах - это и есть кодировка UTF8. Именно так она распознается системой. Сам же Unicode код символа содержится в оставшихся битах двух соседних байтов. Также заметьте код пробела в середине и конец строки в конце - они по-прежнему однобайтные, поскольку все ASCII символы представлены в их оригинальном виде в UTF8 кодировке.

Таким образом, рассматриваемый простейший подход не является правильным решением - это всего лишь небольшая "хитрость", основанная на знании работы системы (и правильности реализации стандартной библиотеки С и эмулятора терминала).

Стандартный подход к выводу русских Unicode символов

Способ, описанный в предыдущем разделе, имеет огромный недостаток - подобный подход не позволит нам использовать поиск, преобразование символов строк и прочие функции стандартной библиотеки С. На мой взгляд, следующий подход является правильным и использует средства из стандарта ANSI C (доступные и в С99).

u@x> cat 3.c
#include <stdio.h> //Стандартный однобайтовый ввод-вывод
#include <wchar.h> //"Широкие" многобайтовые символы и их ввод-вывод
#include <wctype.h> //"Классификация" широких символов
#include <locale.h> //Во избежание "крокозяблей" на выводе

#define MAXSTR 100

int main()
{

int i = 0;
wchar_t wmsg[MAXSTR] = L"Добро Пожаловать!";
// Широкие строчные константы требуют L

if (! setlocale(LC_ALL, "ru_RU.utf8"))
//Используем Линукс с Unicode UTF8 консолью
return 1;

wprintf(L"%ls\n", wmsg);
//Широкие функции требуют L даже в спецификаторе формата.
//С этого момента мы НЕ МОЖЕМ пользоваться printf (и другими однобайтовыми
//функциями ввода-вывода) на потоке stdout.

if (! wscanf(L"%ls", wmsg) ) //Небезопасный ввод
return 2;
else
{
wprintf(L"Вы ввели: %ls\n", wmsg);
//Правильный анализ широких строк возможен ТОЛЬКО с использованием
//многобайтных функций.
//Смотрите ISO/IEC 9899:1999.
if ( iswupper( (wint_t) wmsg[0]) )
wprintf(L"Первая буква Большая!\n");
//Однобайтовый конец строки автоматически переводится в Unicode.

wprintf(L"ЗАГЛАВНЫМИ:\t");
while (wmsg[i] != '\0')
putwchar (towupper (wmsg[i++]) );
putwchar('\n');

}

return 0;
}
u@x>

Строго говоря, все что мы должны были осмыслить это строку из "ISO/IEC 9899:1999", раздел "7.11.1.1 The setlocale function". Стандарт говорит: "At program startup, the equivalent of setlocale(LC_ALL, "C"); is executed". Вот она, причина вопросительных знаков или прочих "крокозяблей" на выводе!

Функции широкого вывода (например, putwchar) получают Unicode символ типа wchar_t во "внутренней" кодировке (4 байта UCS для Линукс, смотрите описание "libc"), перекодируют этот символ для "внешнего" представления в соответствии с текущей локалью ("C" по умолчанию) и выводят в поток stdout, который в нашей системе настроен на "en_EN.utf8". Вот и несоответствие - символ для однобайтной локали "С" выводится в многобайтную локаль UTF8 (и воспринимается ею как пол-символа!).

Решение очевидно - надо всего лишь сказать программе о текущей настройке локали. После этого, мы обязаны использовать только широкие версии функций и широкие типы данных wchar_t и wint_t. Поначалу это раздражает, но незначительное неудобство с лихвой компенсируется возможностью полноценной работы с широкими символами, как показано в примере - буква 'ё' правильно сменила регистр в 'Ё', заглавные буквы верно распознаются стандартными библиотечными функциями.

u@x> file 3.c
3.c: UTF-8 Unicode C program text
u@x> gcc 3.c
u@x> ./a.out
Добро Пожаловать!
Бегемотик
Вы ввели: Бегемотик
Первая буква Большая!
ЗАГЛАВНЫМИ:	БЕГЕМОТИК
u@x> ./a.out
Добро Пожаловать!
поросёнок
Вы ввели: поросёнок
ЗАГЛАВНЫМИ:	ПОРОСЁНОК
u@x>

Также заметьте, что стандартные служебные символы (табуляции, конца строки и пр.) правильно работают в широких константах.

Спасибо что зашли,

Будьте благословенны!
Денис