Ориентация главы
Текстовая магистраль XRT — UTF-8: строковые представления (глава 3) и всё семейство парсеров по умолчанию принимают UTF-8. Но в реальном мире есть три выхода, требующие иных кодировок: API Windows хотят UTF-16, исторические данные могут быть GBK/Latin-1, сетевые протоколы иногда назначают UTF-32 — модуль charset и есть слой преобразования на этих границах. Глава разбирает три вещи: отношение между скалярами и байтами (почему «число символов» отличается от «числа байтов»), перекодирование во все стороны между UTF-8/16/32 (с обёрткой BOM и стратегиями ошибок) и безопасную нарезку по индексам скаляров (без разрезания многобайтовых символов). Функции расстояния и сходства из главы 25 тоже живут здесь — они считают скаляры Unicode, демонстрируя апгрейд «байтового мышления» до «символьного».
27.1引入
Возьмём "XRT 你好 😀": strlen насчитает 14 байтов, но «символов» только 8 — каждый китайский знак весит 3 байта, эмодзи 4. Этот разрыв не академическая мелочь: обрежьте имя пользователя по байтам — и половина китайского знака превратится в мусор; считайте лимит длины по байтам — и китайский пользователь получит на две трети меньше символов, чем англоязычный; сортируйте и сравнивайте по байтам — и порядок не будет иметь ничего общего с ожиданиями человека. Общий корень всех трёх багов: байты принимают за символы.
Центральная абстракция модуля charset — скалярное значение Unicode (кодовая точка) — именно оно является машинным представлением «одного символа» (приблизительно; строго говоря, существуют ещё комбинирующие символы — см. контракты). Байты — форма после кодирования: UTF-8 кодирует скаляр в 1~4 байта, UTF-16 — в 1~2 16-битных юнита (символы дополнительной плоскости требуют «суррогатной пары»), UTF-32 — фиксированно в 4 байта. Все API главы вертятся вокруг превращений «скаляры ↔ различные байтовые формы».
27.2概念
Различия форм трёх кодировок
Замер на "XRT 你好 😀" (вывод главного примера): UTF-16 — 9 юнитов — из 8 скаляров семь занимают по юниту, а 😀 лежит в дополнительной плоскости и занимает суррогатную пару из 2 юнитов. Число юнитов — не число символов, число байтов — тем более — эти три меры (байты/юниты/скаляры) в многоязычном тексте всегда различны; прежде чем отвечать «какой длины», спросите «в чём меряем».
Перекодирование во все стороны и BOM
xrtTranscode преобразует во всех направлениях между UTF-8/16/32 (каждая с LE/BE): вход и выход — байтовые представления, целевая кодировка объявляется перечислением xencoding (порядок байтов тоже в перечислении), обёртка BOM — отдельный булев параметр. Развилка по BOM: протокол уже объявил порядок байтов — не добавляйте (у заголовков HTTP и JSON свои правила); при записи файла на диск добавлять рекомендуется — текстовый редактор откроет без каши.
Стратегии ошибок: STRICT / REPLACE
Что делать с нелегальными последовательностями во входе? Две стратегии, выбор явный: XUTF_STRICT — отказ целиком (слот ошибки запоминает позицию — для валидации границ, где «вход обязан быть легальным»); XUTF_REPLACE — подстановка заменяющего символа U+FFFD (для путей показа — пользователь видит □, а не крушение); других ступеней, кроме STRICT и REPLACE, нет. По умолчанию — STRICT: молчаливая подстановка маскирует ошибку кодирования у вышестоящего источника, REPLACE оставьте для последнего шага, уже точно предназначенного человеческим глазам.
Безопасные операции по скалярам
Байтовый индекс, разрезающий многобайтовый символ посередине, — источник каши. xrtUtf8Slice режет UTF-8 по индексам скаляров — точка разреза автоматически выравнивается на границу символа; xrtUtf8Distance / xrtUtf8Similarity считают расстояние редактирования по скалярам (шпаргалка главы 25 уже предвещала, что они живут здесь); xrtUtf8Count возвращает число скаляров. Всё видимое пользователю — усечение, подсчёт, сравнение — ходит через это семейство; байтовые операции внутренней передачи остаются строковому семейству.
27.3示例
Полная программа: строгий круговой проход UTF-8 ↔ UTF-16 и нарезка по скалярам
Из образца репозитория examples/charset/unicode/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* "XRT 你好 😀":你好 各 3 字节,😀 是 4 字节 emoji。 */ xstrview Text = XRT_STR_LITERAL("XRT \xE4\xBD\xA0\xE5\xA5\xBD \xF0\x9F\x98\x80"); uint16* pUtf16; /* 拥有式:转换出的 UTF-16 数组 */ uint16* pUtf16Copy; /* 拥有式:视图复制产物 */ str sUtf8; /* 拥有式:回转 UTF-8 */ size_t iUnits = 0; /* 出参:UTF-16 单元数 */ xstrview Word; /* 借用:标量切片结果 */ /* 去 UTF-16:严格模式下任何非法序列都会失败并设置线程错误。 */ pUtf16 = xrtUtf8ViewTo16(Text, XUTF_STRICT, &iUnits); if ( pUtf16 == NULL ) { return 1; } */ pUtf16Copy = xrtUtf16DupView(xrtUtf16View(pUtf16, iUnits)); if ( pUtf16Copy == NULL ) { xrtFree(pUtf16); return 1; } /* 回转 UTF-8:长度出参不需要时传 NULL。 */ sUtf8 = xrtUtf16ViewTo8(xrtUtf16View(pUtf16, iUnits), XUTF_STRICT, NULL); if ( sUtf8 == NULL ) { xrtFree(pUtf16Copy); xrtFree(pUtf16); return 1; } printf("UTF-16 units: %llu\n%s\n", (unsigned long long)iUnits, sUtf8); */ if ( !xrtUtf8Slice(Text, 4, 2, &Word) ) { xrtFree(sUtf8); xrtFree(pUtf16Copy); xrtFree(pUtf16); return 1; } printf("scalar slice: %.*s\n", (int)Word.Size, Word.Data); /* 三个拥有式资源逆序释放。 */ xrtFree(sUtf8); xrtFree(pUtf16Copy); xrtFree(pUtf16); return 0; }
Что только что произошло. (1) xrtUtf8ViewTo16 превращает представление UTF-8 в владеющий массив UTF-16 (строгий режим); выходной параметр выносит число юнитов 9 — не 8: суррогатная пара 😀 занимает 2 юнита, нагляднейшее учебное пособие по тезису «число юнитов ≠ число скаляров». (2) xrtUtf16ViewTo8 возвращает обратно, вывод не изменился — круговой проход без потерь, строгий режим гарантирует легальность последовательностей в обе стороны. (3) xrtUtf8Slice по индексам скаляров вырезает 你好 — разрез ложится на границу символа; даже если переданный байтовый номер попадёт на средний байт китайского знака, функция выровняет на границу, а не отрежет полсимвола. Стандартное употребление на границе Windows — ровно эти два шага: To16 перед входом в системный вызов, To8 после возврата.
Полная программа: перекодирование с обёрткой BOM
Из examples/charset/transcode/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 源文本 "XRT 你好"(UTF-8 字面量,"你好"各占 3 字节)。 */ xbytesview Source = XRT_BYTES_LITERAL("XRT \xE4\xBD\xA0\xE5\xA5\xBD"); bytes pUtf16; /* 拥有式:转换产物 */ size_t iSize = 0; */ pUtf16 = xrtTranscode(Source, XENCODING_UTF8, XENCODING_UTF16_LE, XUTF_STRICT, true, &iSize); if ( pUtf16 == NULL ) { return 1; } /* 打印产物总字节数(含 BOM),证明转换真实发生。 */ printf("UTF-16 LE bytes with BOM: %llu\n", (unsigned long long)iSize); /* 拥有式产物由 xrtFree 释放。 */ xrtFree(pUtf16); return 0; }
Что только что произошло. (1) xrtTranscode нацелен на UTF-16 LE с BOM — выходной поток начинается с FF FE, приёмщик с первого взгляда распознаёт кодировку и порядок байтов. (2) Число байтов 14 вычислимо в столбик: 6 кодовых точек внутри BMP по 2 байта = 12 плюс 2 байта BOM — размер выхода перекодирования выводим, планирование буфера без гаданий. (3) Всенаправленность означает, что UTF-32 → UTF-8, Latin-1 → UTF-16 и прочие комбинации идут через один вход — параметры объявляют кодировки обоих концов, отдельная функция на пару не нужна.
27.4契约
- Магистраль UTF-8: строковое семейство XRT и парсеры по умолчанию на UTF-8; преобразования случаются только на границах системы и протоколов.
- Строгость по умолчанию: нелегальные последовательности — отказ целиком через
XUTF_STRICT; REPLACE — явный выбор и только для путей показа. - Три меры: байты / юниты UTF-16 / скаляры всегда могут разойтись — сперва вопрос о мере, потом о длине.
- Безопасность скаляров: видимое пользователю усечение/подсчёт/сравнение — семейство Utf8 по скалярам; байтовые операции — для внутренней передачи.
- Владеющие продукты: перекодирование и To16/To8 возвращают владеющие массивы, освобождение по их объявлению (
xrtFree). - Развилка BOM: протокол объявил — не добавляем; файл на диск — рекомендуется добавить.
Потоковое декодирование и инкрементальная валидация
Данные, приходящие кусками (сетевые потоки, большие файлы), не могут ждать «соберём все байты, потом проверим» — многобайтовый символ может попасть ровно на шов между кусками. Для этого существует потоковый декодер xutf8state: xrtUtf8StateInit открывает, xrtUtf8StateFeed скармливает по кускам (флаг bFinal помечает последний), xrtUtf8StateError читает позицию ошибки — незавершённый символ на шве запоминается машиной состояний и не признаётся нелегальным по ошибке. Здесь сходятся «строгая валидация» и «потоковая обработка»; это форма понадобится и читателю строк в главе 34, и протокольным потокам тома 7. Корректная проверка легальности кодировки большого файла — именно она: потоковая подача, проверка состояния после последнего куска, ноль буферизации файла целиком.
Комбинирующие символы и воспринимаемые пользователем символы
Глава аккуратно говорит «скаляр», а не «символ», потому что в Unicode это не всегда одно и то же: буква с диакритикой может быть двумя скалярами («базовая буква + комбинирующий знак»), а у эмодзи есть ZWJ-последовательности (семьи, профессии) до десятка с лишним скаляров. Подсчёта по скалярам достаточно для подавляющего большинства задач, но «число воспринимаемых пользователем символов» (grapheme cluster) — более строгая мера — подсказка «осталось N символов» у поля ввода и шаг курсора, строго говоря, должны идти по кластерам. Позиция XRT: слой скаляров — общий фундамент (семейство этой главы); потребности уровня кластеров обрабатываются по мере надобности в слое показа (большинству проектов хватит скаляров, апгрейд — когда придут жалобы; но знать, что разница существует, нужно, чтобы узнать её при встрече). Запомните этот абзац: когда однажды придёт жалоба «длина неправильная», вы будете знать, куда смотреть — это же объясняет, почему глава последовательно пользуется точным словом «скаляр», а не удобным «символом».
Переподтверждение разделения труда с главой 25
Разделение одной фразой: байтовые операции — к string (глава 25), символьные операции — к charset (эта глава). Поиск, разрезание, склейка — эта «обработка содержимого» работает по байтам и безопасна под UTF-8 (разделители обычно ASCII); подсчёт, усечение, ширина, расстояние — эти «видимые пользователю» операции обязаны идти по скалярам через семейство этой главы. У двух семейств один тип представления (xstrview) — они сшиваются бесшовно: найти по байтам через Find, усечь по скалярам через Slice — стандартная поза смешивания.
Типичные ошибки
Симптом: в хвосте китайского имени или имени с эмодзи изредка появляется мусор или �; детерминированно воспроизводится, когда конкретная длина режет многобайтовый символ посередине.
Причина: в UTF-8 один символ занимает 1~4 байта, и у байтового индекса нет понятия границы символа — разрез в середине порождает нелегальную последовательность.
if ( strlen(sName) > 32 ) { sName[32] = 0; /* усечение на уровне байтов: может отрезать половину символа */ }
xstrview Name = (xstrview){ sName, strlen(sName) }; if ( xrtUtf8Count(Name) > 16 ) { Name = xrtUtf8Slice(Name, 0, 16); /* усечение по скалярам, граница выровнена автоматически */ } /* Name - представление: Dup-ните, когда понадобится нулевое завершение */
Симптом: после круговорота многоязычных данных часть символов превращается в � или исчезает бесследно; ошибка кодирования у источника так никогда и не находится.
Причина: стратегии REPLACE/IGNORE «разбираются» с нелегальным входом — вывод выглядит нормально, данные уже испорчены, и никто не подал сигнала.
/* REPLACE для граничной валидации - испорченные данные от источника тихо отмываются */ xrtTranscode(Input, XENCODING_UTF8, XENCODING_UTF16_LE, XUTF_REPLACE, true, ...);
/* на границе системы сперва строгая проверка, преобразование - после подтверждения легальности */ if ( xrtTranscode(Input, XENCODING_UTF8, XENCODING_UTF16_LE, XUTF_STRICT, true, ...) == NULL ) { RejectInput(); /* нелегальная последовательность отвергнута здесь - баг всплывает у истока */ }
Практические задания
Для "XRT 你好 😀" выведите число байтов UTF-8, число юнитов UTF-16 и число скаляров (14 / 9 / 8); затем соберите чисто эмодзи-строку и чисто китайскую строку и повторите замер — расхождение трёх мер меняется с составом содержимого; запишите по столбику цифр на каждую — стоит своими глазами прогнать несколько наборов.
Реализуйте truncate_utf8(视图, 最大标量数) (представление, максимум скаляров): усечение по скалярам, выравнивание на границы, при превышении — многоточие в конец (… тоже весит 3 байта, не забудьте учесть). Проверьте отсутствие каши на десяти входах со смесью китайского, латиницы и эмодзи.
Напишите пару open_utf8(路径)/read_dir_utf8() (путь): внутри путь переводится в UTF-16 и вызываются широкосимвольные системные API, возвращённый UTF-16 переводится обратно в UTF-8 — строгий режим, нелегальные последовательности с позицией в отчёте. Критерии приёмки: создание/открытие/перечисление китайских путей проходит весь круг без потерь; сконструируйте нелегальный путь с одиночным суррогатным байтом — отчёт об ошибке укажет позицию проблемного юнита.
Итоги
| Тема | Кратко |
|---|---|
| Магистраль | UTF-8; преобразования только на границах системы/протокола, по одному разу с каждой стороны |
| Три меры | байты / юниты UTF-16 / скаляры — сперва вопрос о мере, потом ответ о длине |
| Перекодирование | xrtTranscode во все стороны; BOM параметризован; размер выхода выводим |
| Стратегия ошибок | STRICT по умолчанию (валидация границ); REPLACE явно для путей показа |
| Безопасность скаляров | xrtUtf8Slice/Utf8Count/Utf8Distance работают по символам |
| Платформенная граница | API Windows — вход через To16, выход через To8 |
| BOM | протокол объявил порядок байтов — не добавляем; файл на диск — рекомендуем (независимый булев параметр) |
| Потоковость | xutf8state проверяет по кускам (Init/Feed/Error — три шага); шов между кусками не даёт ложных срабатываний |
| Мера символа | скаляр ≠ воспринимаемый пользователем символ (кластер); бизнесу чаще хватает скаляров, слой показа апгрейдится по надобности |
📖 Справочники API в этой главе:charset (81 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки