Программирование на C: как программировать для Unicode?

83

Какие предварительные условия необходимы для выполнения строгого программирования Unicode?

Означает ли это, что мой код charнигде не должен использовать типы и что нужно использовать функции, которые могут иметь дело с wint_tи wchar_t?

И какую роль в этом сценарии играют многобайтовые последовательности символов?

Prinzdezibel
источник

Ответы:

21

Обратите внимание, что речь идет не о «строгом программировании в Юникоде» как таковом, а о некотором практическом опыте.

В моей компании мы создали библиотеку-оболочку для библиотеки IBM ICU. Библиотека-оболочка имеет интерфейс UTF-8 и преобразуется в UTF-16, когда необходимо вызвать ICU. В нашем случае мы не особо беспокоились о падении производительности. Когда производительность была проблемой, мы также предоставляли интерфейсы UTF-16 (с использованием нашего собственного типа данных).

Приложения могут оставаться в основном как есть (с использованием char), хотя в некоторых случаях им необходимо знать об определенных проблемах. Например, вместо strncpy () мы используем оболочку, которая избегает обрезания последовательностей UTF-8. В нашем случае этого достаточно, но можно также рассмотреть проверки на объединение символов. У нас также есть обертки для подсчета количества кодовых точек, количества графем и т. Д.

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

Мы не используем wchar_t. Использование ICU позволяет избежать неожиданных проблем с переносимостью (но, конечно, не других неожиданных проблем :-).

Ханс ван Эк
источник
2
Действительная последовательность байтов UTF-8 никогда не будет обрезана (усечена) с помощью strncpy. Допустимые последовательности UTF-8 не могут содержать байтов 0x00 (за исключением, конечно, завершающего нулевого байта).
Дэн Молдинг,
8
@Dan Molding: если вы используете strncpy (), скажем, строку, содержащую один китайский символ (который может быть 3 байта) в 2-байтовом массиве символов, вы создаете недопустимую последовательность UTF-8.
Ханс ван Экк,
@Hans van Eck: Если ваша оболочка копирует этот единственный 3-байтовый китайский символ в 2-байтовый массив, вы либо собираетесь его усечь и создать недопустимую последовательность, либо у вас будет неопределенное поведение. Очевидно, что если вы копируете данные, цель должна быть достаточно большой; само собой разумеется. Я хотел сказать, что strncpyпри правильном использовании совершенно безопасно использовать с UTF-8.
Дэн Молдинг
5
@DanMoulding: если вы знаете, что ваш целевой буфер достаточно велик, вы можете просто использовать strcpy(что действительно безопасно для использования с UTF-8). Люди, использующие, strncpyвероятно, делают это, потому что они не знают, достаточно ли большой целевой буфер, поэтому они хотят передать максимальное количество байтов для копирования, что действительно может создать недопустимые последовательности UTF-8.
Frerich Raabe
42

C99 или ранее

Стандарт C (C99) предусматривает использование широких символов и многобайтовых символов, но поскольку нет гарантии, что эти широкие символы могут содержать, их значение несколько ограничено. Для данной реализации они обеспечивают полезную поддержку, но если ваш код должен иметь возможность перемещаться между реализациями, нет достаточной гарантии, что они будут полезны.

Следовательно, подход, предложенный Хансом ван Экком (который заключается в написании оболочки для библиотеки ICU - International Components for Unicode), является правильным, IMO.

Кодировка UTF-8 имеет множество достоинств, одно из которых заключается в том, что если вы не вмешиваетесь в данные (например, усекая их), то их можно скопировать функциями, которые не полностью осведомлены о тонкостях UTF-8. кодирование. Это категорически не относится к wchar_t.

Unicode полностью - это 21-битный формат. То есть Unicode резервирует кодовые точки от U + 0000 до U + 10FFFF.

Одна из полезных особенностей форматов UTF-8, UTF-16 и UTF-32 (где UTF означает формат преобразования Unicode - см. Unicode ) заключается в том, что вы можете конвертировать между тремя представлениями без потери информации. Каждый может представлять все, что могут представлять другие. И UTF-8, и UTF-16 являются многобайтовыми форматами.

UTF-8 хорошо известен как многобайтовый формат с тщательно продуманной структурой, которая позволяет надежно находить начало символов в строке, начиная с любой точки строки. У однобайтовых символов старший бит установлен в ноль. Многобайтовые символы имеют первый символ, начинающийся с одного из битовых шаблонов 110, 1110 или 11110 (для 2-байтовых, 3-байтовых или 4-байтовых символов), а последующие байты всегда начинаются с 10. Символы продолжения всегда находятся в диапазон 0x80 .. 0xBF. Существуют правила, согласно которым символы UTF-8 должны быть представлены в минимально возможном формате. Одним из следствий этих правил является то, что байты 0xC0 и 0xC1 (также 0xF5..0xFF) не могут появляться в действительных данных UTF-8.

Первоначально предполагалось, что Unicode будет 16-битным кодовым набором, и все будет помещено в 16-битное кодовое пространство. К сожалению, реальный мир более сложен, и его пришлось расширить до нынешней 21-битной кодировки.

UTF-16, таким образом, представляет собой единый кодовый блок (16-битное слово), установленный для «Базовой многоязычной плоскости», то есть символы с кодовыми точками Unicode U + 0000 .. U + FFFF, но использует две единицы (32-битные) для символы вне этого диапазона. Таким образом, код, работающий с кодировкой UTF-16, должен иметь возможность обрабатывать кодировки переменной ширины, как и UTF-8. Коды для двухзначных символов называются суррогатами.

Суррогаты - это кодовые точки из двух специальных диапазонов значений Unicode, зарезервированные для использования в качестве начального и конечного значений парных кодовых единиц в UTF-16. Ведущие, также называемые высокими суррогатами - от U + D800 до U + DBFF, а замыкающие или нижние суррогаты - от U + DC00 до U + DFFF. Их называют суррогатами, поскольку они не представляют персонажей напрямую, а только в виде пары.

UTF-32, конечно, может кодировать любую кодовую точку Unicode в единой единице хранения. Он эффективен для вычислений, но не для хранения.

Вы можете найти гораздо больше информации на сайтах ICU и Unicode.

C11 и <uchar.h>

Стандарт C11 изменил правила, но даже сейчас (середина 2017 года) не все реализации учли эти изменения. Стандарт C11 суммирует изменения для поддержки Unicode следующим образом:

  • Символы и строки Unicode ( <uchar.h>) (изначально указаны в ISO / IEC TR 19769: 2004)

Далее следует лишь минимальный набросок функциональности. В спецификацию входят:

6.4.3 Универсальные имена персонажей

Синтаксис имя-
универсального-символа:
    \u шестнадцатеричный-четверной
    \U шестнадцатеричный шестнадцатеричный
шестнадцатеричный шестнадцатеричный:
     шестнадцатеричная цифра шестнадцатеричная цифра шестнадцатеричная цифра шестнадцатеричная цифра

7.28 Утилиты Unicode <uchar.h>

Заголовок <uchar.h> объявляются типы и функции для управления символами Unicode.

Объявленные типы mbstate_t(описаны в 7.29.1) и size_t(описаны в 7.19);

который является беззнаковым целочисленным типом, используемым для 16-битных символов, и имеет тот же тип, что и uint_least16_t(описанный в 7.20.1.2); и

который является беззнаковым целочисленным типом, используемым для 32-битных символов, и имеет тот же тип, что и uint_least32_t(также описанный в 7.20.1.2).

(Перевод перекрестных ссылок: <stddef.h>определяет size_t, <wchar.h>определяет mbstate_tи <stdint.h>определяет uint_least16_tи uint_least32_t.) <uchar.h>Заголовок также определяет минимальный набор (перезапускаемых) функций преобразования:

  • mbrtoc16()
  • c16rtomb()
  • mbrtoc32()
  • c32rtomb()

Существуют правила относительно того, какие символы Unicode могут использоваться в идентификаторах с использованием нотации \unnnnили \U00nnnnnn. Возможно, вам придется активно активировать поддержку таких символов в идентификаторах. Например, GCC требует -fextended-identifiersразрешить это в идентификаторах.

Обратите внимание, что macOS Sierra (10.12.5), если назвать только одну платформу, не поддерживает <uchar.h>.

Джонатан Леффлер
источник
3
Думаю, вы продаете wchar_tи друзьям здесь немного не хватает. Эти типы необходимы для того, чтобы библиотека C могла обрабатывать текст в любой кодировке (включая кодировки, отличные от Unicode). Без широких символьных типов и функций библиотеке C потребовался бы набор функций обработки текста для каждой поддерживаемой кодировки: представьте, что у вас есть koi8len, koi8tok, koi8printf только для текста в кодировке KOI-8 и utf8len, utf8tok, utf8printf для UTF-8. текст. Вместо этого, нам повезло иметь только один набор этих функций (не считая первоначально одни ASCII): wcslen, wcstok, и wprintf.
Дэн Молдинг,
1
Все, что нужно сделать программисту, - это использовать функции преобразования символов библиотеки C ( mbstowcsи их друзей) для преобразования любой поддерживаемой кодировки в wchar_t. После wchar_tформатирования программист может использовать единый набор широких функций обработки текста, которые предоставляет библиотека C. Хорошая реализация библиотеки C будет поддерживать практически любую кодировку, которая когда-либо понадобится большинству программистов (в одной из моих систем у меня есть доступ к 221 уникальной кодировке).
Дэн Молдинг,
Что касается того, будут ли они достаточно широкими, чтобы быть полезными: стандарт требует, чтобы реализация была wchar_tдостаточно широкой, чтобы содержать любой символ, поддерживаемый реализацией. Это означает (возможно, с одним заметным исключением) большинство реализаций будут гарантировать, что они достаточно широки, чтобы используемая программа могла wchar_tобрабатывать любую кодировку, поддерживаемую системой ( wchar_tширина Microsoft составляет всего 16 бит, что означает, что их реализация не полностью поддерживает все кодировки, прежде всего различные кодировки UTF, но они являются исключением, а не правилом).
Дэн Молдинг,
11

Этот FAQ содержит большое количество информации. Между этой страницей и этой статьей Джоэла Спольски у вас будет хорошее начало.

Один вывод, к которому я пришел по пути:

  • wchar_t- это 16 бит в Windows, но не обязательно 16 бит на других платформах. Я думаю, что это необходимое зло для Windows, но, вероятно, его можно избежать в другом месте. Причина, по которой это важно в Windows, заключается в том, что вам нужно использовать файлы, в имени которых есть символы, отличные от ASCII (вместе с версией функций W).

  • Обратите внимание, что API-интерфейсы Windows, принимающие wchar_tстроки, ожидают кодировки UTF-16. Также обратите внимание, что это отличается от UCS-2. Обратите внимание на суррогатные пары. Эта тестовая страница содержит полезные тесты.

  • Если вы программируете на Windows, вы не можете использовать fopen(), fread(), fwrite()и т.д. , так как они только принимают char *и не понимают кодировку UTF-8. Делает переносимость болезненной.

dbyron
источник
Обратите внимание , что STDIO f*и друзья работают с char *на каждой платформе , потому что стандарт говорит так - использовать wcs*вместо этого для wchar_t.
кот
7

Чтобы выполнить строгое программирование Unicode:

  • Используйте только строковые API - интерфейсы, которые Unicode известно ( НЕ strlen , strcpy... но их WideString коллеги wstrlen, wsstrcpy...)
  • При работе с блоком текста используйте кодировку, которая позволяет сохранять символы Unicode (utf-7, utf-8, utf-16, ucs-2, ...) без потерь.
  • Убедитесь, что набор символов вашей ОС по умолчанию совместим с Unicode (например, utf-8)
  • Используйте шрифты, совместимые с Unicode (например, arial_unicode)

Многобайтовые последовательности символов - это кодировка, которая предшествует кодировке UTF-16 (обычно используемой wchar_t), и мне кажется, что она скорее предназначена только для Windows.

Я никогда не слышал wint_t.

Себастьен
источник
wint_t - это тип, определенный в <wchar.h>, как и wchar_t. Он имеет ту же роль по отношению к широким символам, что и int по отношению к 'char'; он может содержать любое значение широких символов или WEOF.
Джонатан Леффлер,
3

Самое главное - всегда четко различать текстовые и двоичные данные . Попробуйте следовать модели Python 3.x strvs.bytes или SQL TEXTvs BLOB..

К сожалению, C сбивает с толку, используя charкак «символ ASCII», так и int_least8_t. Вы захотите сделать что-то вроде:

Вам могут понадобиться typedef для кодовых единиц UTF-16 и UTF-32, но это более сложно, потому что кодировка wchar_tне определена. Вам понадобится только препроцессор #if. Вот некоторые полезные макросы в C и C ++ 0x:

  • __STDC_UTF_16__- Если определено, тип _Char16_tсуществует и является UTF-16.
  • __STDC_UTF_32__- Если определено, тип _Char32_tсуществует и является UTF-32.
  • __STDC_ISO_10646__- Если определено, то wchar_tиспользуется UTF-32.
  • _WIN32- В Windows wchar_tиспользуется UTF-16, даже если это нарушает стандарт.
  • WCHAR_MAX- Может использоваться для определения размера wchar_t, но не для определения того , использует ли ОС его для представления Unicode.

Означает ли это, что мой код нигде не должен использовать типы char и что необходимо использовать функции, которые могут иметь дело с wint_t и wchar_t?

Смотрите также:

Нет. UTF-8 - это вполне допустимая кодировка Unicode, в которой используются char*строки. Его преимущество заключается в том, что если ваша программа прозрачна для байтов, отличных от ASCII (например, конвертер окончания строки, который действует на другие символы \rи \nпропускает их через другие символы без изменений), вам нужно вообще не вносить никаких изменений!

Если вы выберете UTF-8, вам нужно будет изменить все предположения, что charсимвол = (например, не вызывать toupperв цикле) или charстолбец = screen (например, для переноса текста).

Если вы выберете UTF-32, у вас будет простота символов фиксированной ширины (но не графем фиксированной ширины). , но вам нужно будет изменить тип всех ваших строк).

Если вы выберете UTF-16, вам придется отказаться от использования символов фиксированной ширины и от предположения о 8-битных единицах кода, что делает этот путь наиболее сложным путем обновления однобайтовых кодировок.

Я бы рекомендовал активно избегать, wchar_t потому что это не кросс-платформенный: иногда это UTF-32, иногда это UTF-16, а иногда это восточноазиатская кодировка до Unicode. Я бы рекомендовал использоватьtypedefs

Еще важнее избегатьTCHAR .

dan04
источник
Я не думаю, что это вообще прискорбно - char является int. Это преимущество. На ум приходит использование буквальных символьных констант. И функции, которые принимают a, char *могут иметь проблемы, если они переданы const char *последним, о котором я помню (но я не уверен в этом и о том, какие функции, поэтому относитесь к этому с щепоткой соли). Тот факт, что с другими языками сложнее, не означает, что это плохой дизайн.
Pryftan
2

Я бы не стал доверять любой стандартной реализации библиотеки. Просто используйте свои собственные типы юникода.


источник
2

В основном вы хотите иметь дело со строками в памяти как с wchar_tмассивами, а не с символами. Когда вы выполняете какой-либо ввод-вывод (например, чтение / запись файлов), вы можете кодировать / декодировать с помощью UTF-8 (это, вероятно, наиболее распространенная кодировка), которую достаточно просто реализовать. Просто погуглите RFC. Так что в памяти ничего не должно быть многобайтовым. Один wchar_tпредставляет одного персонажа. Однако когда вы переходите к сериализации, вам нужно кодировать что-то вроде UTF-8, где некоторые символы представлены несколькими байтами.

Вам также придется писать новые версии strcmpи т. Д. Для строк широких символов, но это не большая проблема. Самая большая проблема будет связана с взаимодействием с библиотеками / существующим кодом, которые принимают только массивы символов.

А когда дело доходит до sizeof(wchar_t)(вам потребуется 4 байта, если вы хотите сделать это правильно), вы всегда можете переопределить его на больший размер с помощью typedef/ macrohacks, если вам нужно.

Майк Веллер
источник
1

Насколько я знаю, wchar_t зависит от реализации (как видно из этой статьи в вики ). И это не юникод.

PolyThinker
источник