Ориентация главы

Текстовая магистраль 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概念

Различия форм трёх кодировок

1UTF-8скаляры кодируются в 1~4 байта; совместимость с ASCII; магистраль XRT и умолчание сети
2UTF-16скаляры кодируются в 1~2 16-битных юнита; дополнительная плоскость (как 😀) требует суррогатной пары
3UTF-32скаляры фиксированно 4 байта; удобно для внутренних вычислений, редкость при передаче

Замер на "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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/charset/unicode/main.c -lws2_32 -liphlpapiUTF-16 units: 9XRT 你好 😀scalar slice: 你好

Что только что произошло. (1) xrtUtf8ViewTo16 превращает представление UTF-8 в владеющий массив UTF-16 (строгий режим); выходной параметр выносит число юнитов 9 — не 8: суррогатная пара 😀 занимает 2 юнита, нагляднейшее учебное пособие по тезису «число юнитов ≠ число скаляров». (2) xrtUtf16ViewTo8 возвращает обратно, вывод не изменился — круговой проход без потерь, строгий режим гарантирует легальность последовательностей в обе стороны. (3) xrtUtf8Slice по индексам скаляров вырезает 你好 — разрез ложится на границу символа; даже если переданный байтовый номер попадёт на средний байт китайского знака, функция выровняет на границу, а не отрежет полсимвола. Стандартное употребление на границе Windows — ровно эти два шага: To16 перед входом в системный вызов, To8 после возврата.

Полная программа: перекодирование с обёрткой BOM

Из examples/charset/transcode/main.c:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/charset/transcode/main.c -lws2_32 -liphlpapiUTF-16 LE bytes with BOM: 14

Что только что произошло. (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 — стандартная поза смешивания.

Типичные ошибки

Ловушка 1: усечение пользовательского текста по байтам:Ловушка 1: усечение пользовательского текста по байтам

Симптом: в хвосте китайского имени или имени с эмодзи изредка появляется мусор или �; детерминированно воспроизводится, когда конкретная длина режет многобайтовый символ посередине.

Причина: в 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-ните, когда понадобится нулевое завершение */
Ловушка 2: ошибки перекодирования молча проглатываются:Ловушка 2: ошибки перекодирования молча проглатываются

Симптом: после круговорота многоязычных данных часть символов превращается в � или исчезает бесследно; ошибка кодирования у источника так никогда и не находится.

Причина: стратегии 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 байта, не забудьте учесть). Проверьте отсутствие каши на десяти входах со смесью китайского, латиницы и эмодзи.

Вызов: мост к именам файлов Windows

Напишите пару 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки