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


83

Які передумови необхідні для жорсткого програмування Unicode?

Чи означає це, що мій код не повинен використовувати charтипи ніде, і що потрібно використовувати функції, які можуть мати справу з wint_tі wchar_t?

І яку роль у цьому сценарії відіграють багатобайтові послідовності символів?

Відповіді:


21

Зверніть увагу, що мова йде не про "суворе програмування Unicode" як таке, а про певний практичний досвід.

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

Програми можуть залишатися в основному як є (із використанням символу char), хоча в деяких випадках їм потрібно знати про певні проблеми. Наприклад, замість strncpy () ми використовуємо обгортку, яка уникає обрізання послідовностей UTF-8. У нашому випадку цього достатньо, але можна також розглянути перевірки на поєднання символів. У нас також є обгортки для підрахунку кількості кодових точок, кількості графем тощо.

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

Ми не використовуємо wchar_t. Використання ICU дозволяє уникнути несподіваних проблем із портативністю (але, звичайно, не інших несподіваних проблем :-).


2
Дійсна послідовність байтів UTF-8 ніколи не буде обрізана (усічена) за допомогою strncpy. Дійсні послідовності UTF-8 не можуть містити 0x00 байт (крім нульового байта, що завершується, звичайно).
Dan Molding

8
@Dan Molding: якщо ви strncpy (), скажімо, рядок, що містить один китайський символ (який може становити 3 байти) у 2-байтовий масив символів, ви створите недійсну послідовність UTF-8.
Ганс ван Ек

@Hans van Eck: Якщо ваша обгортка копіює цей єдиний 3-байтовий китайський символ у 2-байтовий масив, тоді ви або збираєтесь його скоротити і створити недійсну послідовність, або у вас буде невизначена поведінка. Очевидно, що якщо ви копіюєте дані навколо, ціль повинна бути досить великою; це само собою зрозуміло. Моя думка полягала в тому, що strncpyналежне використання є абсолютно безпечним для використання з UTF-8.
Dan Molding

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 hex-quad
    \U hex-quad hex-quad
hex-quad:
    hexadecimal-digit hexadecimal-digit hexadecimal-digit hexadecimal-digit

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.
Dan Molding

1
Все, що потрібно зробити програмісту, - це використовувати функції перетворення символів бібліотеки C ( mbstowcsі друзів) для перетворення будь-якого підтримуваного кодування в wchar_t. Отримавши wchar_tформат, програміст може використовувати єдиний набір функцій широкої обробки тексту, що надається бібліотекою C. Хороша реалізація бібліотеки C підтримуватиме практично будь-яке кодування, яке колись знадобиться більшості програмістів (в одній з моїх систем я маю доступ до 221 унікального кодування).
Dan Molding

Що стосується того, чи будуть вони достатньо широкими, щоб бути корисними: стандарт вимагає, щоб реалізація повинна гарантувати, що вона wchar_tє достатньо широкою, щоб містити будь-який символ, що підтримується реалізацією. Це означає (за винятком одного помітного винятку), що більшість реалізацій забезпечать їх достатню ширину, щоб програма, яка використовує wchar_t, обробляла будь-яке кодування, яке підтримується системою ( wchar_tширина Microsoft має лише 16 біт, що означає, що їх реалізація не повністю підтримує всі кодування, перш за все різні кодування UTF, але їхнє є винятком, а не правилом).
Dan Molding

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. Робить портативність болючою.


Зверніть увагу , що 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 strпротиbytes або SQL TEXTпроти BLOB.

На жаль, C плутає проблему, використовуючи charяк "ASCII символ", так і int_least8_t. Ви захочете зробити щось на зразок:

Можливо, вам потрібні typedefs для кодових одиниць UTF-16 та UTF-32, але це складніше, оскільки кодування wchar_tне визначено. Вам знадобиться лише препроцесор #ifs. Деякі корисні макроси в 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_tUTF-16, хоча це порушує стандарт.
  • WCHAR_MAX- Може використовуватися для визначення розміру wchar_t, але не для того, чи використовує його ОС для представлення Unicode.

Чи означає це, що мій код ніде не повинен використовувати типи символів і що потрібно використовувати функції, які можуть мати справу з wint_t та wchar_t?

Дивитися також:

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

Якщо ви використовуєте UTF-8, вам доведеться змінити всі припущення, що char= символ (наприклад, не викликати toupperв циклі) або char= екранний стовпець (наприклад, для обтікання тексту).

Якщо ви використовуєте UTF-32, ви отримаєте простоту символів із фіксованою шириною (але не графем з фіксованою шириною , але вам потрібно буде змінити тип усіх ваших рядків).

Якщо ви користуєтесь UTF-16, вам доведеться відкинути як припущення про символи фіксованої ширини, так і припущення про 8-бітові кодові одиниці, що робить це найскладнішим шляхом оновлення з однобайтових кодувань.

Я б рекомендував активно уникати, wchar_t оскільки це не крос-платформа: іноді це UTF-32, іноді UTF-16, а іноді це східноазіатське кодування до Юнікоду. Я б рекомендував використовуватиtypedefs

Ще важливіше уникатиTCHAR .


Я не думаю, що це взагалі прикро - символ, який є int. Це вигода. Використання буквальних констант символів спадає на думку як одне використання. І функції, які приймають, char *можуть мати проблеми, якщо я пройшов const char *останнє, що я пам’ятаю (але я розмитий щодо цього, і які функції, тому приймайте це з дрібкою солі). Те, що це складніше з іншими мовами, не означає, що це поганий дизайн.
Прифтан

2

Я б не довіряв будь-якій стандартній реалізації бібліотеки. Просто розкачайте власні типи Unicode.


2

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

Вам також доведеться писати нові версії strcmpтощо для широких рядків символів, але це не велика проблема. Найбільшою проблемою буде взаємодія з бібліотеками / існуючим кодом, які приймають лише масиви char.

І коли справа доходить до sizeof(wchar_t)(вам потрібно 4 байти, якщо ви хочете зробити це правильно), ви завжди можете перевизначити його до більшого розміру за допомогою typedef/ macroхакі, якщо вам потрібно.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.