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

JSON — лингва франка современного обмена данными, и XRT даёт ему три полных маршрута: DOM (xrtJsonParse в дерево xvalue — умолчание для конфигураций и небольших документов), SAX (xrtJsonVisit — потоковый обход событиями через обратный вызов, ноль промежуточного дерева для больших файлов) и Writer (xrtJsonWriterCreate — инкрементная сборка: потоковая сериализация без накопления целого дерева). Три маршрута разделяют один комплект строгости: дубликаты ключей отвергаются, границы ресурсов (три шлюза главы 3) защищают от бомб глубокой вложенности, ошибки несут строку и столбец (xrtJsonErrorLocation). Глава проясняет подходящие сценарии каждого маршрута и даёт правила выбора.

32.1引入

Три сценария — три маршрута. Загрузка конфигурации: на старте читается JSON на несколько сотен строк — маршрут DOM это три строки кода (Parse, взять по имени, Release по завершении), дерево значений напрямую входит в систему слияния/чтения главы 30. Скан журналов: проход по JSON Lines-журналу в 2 ГБ в поисках аномалий — DOM превратил бы весь файл в дерево (взрыв памяти), SAX же вызывает обратный вызов событие за событием, ноль промежуточных структур, память не зависит от размера файла. Сериализация ответа: сервер записывает результат запроса как JSON-ответ — данные уже лежат в бизнес-структурах, строить под сериализацию дерево значений расточительно; Writer пишет инкрементно: открыть объект, вывести пары ключ-значение, закрыть объект — генерация и вывод идут рядом.

Три маршрута — не эзотерика выбора одного из трёх, а ответы на три вопроса о форме данных: нужен произвольный доступ и повторное чтение → DOM; один проход сканирования → SAX; вывод по мере генерации → Writer. Сосуществование всех трёх в одной системе — норма: конфигурация через DOM, журналы через SAX, ответы через Writer, каждый на своём месте.

32.2概念

DOM: разбор, доступ, сериализация

1РазборxrtJsonParse(текст) -> владеющий корневой объект (освобождение Release)
2ДоступObjectGet берёт члена по имени (заимствование) -> точное чтение (глава 30)
3СериализацияxrtJsonStringify(значение, bPretty) -> владеющий текст
4Строгостьдубликаты ключей отвергаются; три шлюза границ ресурсов; ошибка со строкой и столбцом

Ценность DOM — «разобрали один раз, доступ произвольный»: дерево значений в памяти, переход куда угодно по путям. При отказе xrtJsonParse в слоте ошибки потока структурированная запись, а xrtJsonErrorLocation выносит строку и столбец — «строка 3, столбец 14, не хватает запятой» указывает прямо на символ. Сериализация чисел с плавающей точкой идёт кратчайшим круговым проходом главы 26 — прочтённое 0.1 записывается обратно как 0.1, круг без дрейфа.

SAX: поток событий

xrtJsonVisit(文本, 回调, 用户数据) (текст, обратный вызов, пользовательские данные) превращает разбор в последовательность событий: вход в объект/массив, имя члена, скалярное значение, конец контейнера. Обратный вызов получает структуру xjsonevent (HasName/Name/тип/значение) и возвращает false для прерывания. Ноль промежуточного дерева — его преимущество формы: память — это стек самого обратного вызова; в паре с потоковым декодированием главы 27 получается «разбирать по мере приёма». Дисциплины SAX: не делать тяжёлой работы в обратном вызове (он на пути разбора) и не хранить заимствованные указатели событий дольше времени жизни вызова (дисциплина сроков главы 30).

Writer: инкрементная сборка

xjsonwriter* W = xrtJsonWriterCreate(NULL);
xrtJsonWriterObject(W);          /* открыть объект */
xrtJsonWriterName(W, "name");
xrtJsonWriterString(W, ...);
xrtJsonWriterEnd(W);             /* закрыть объект */
str s = xrtJsonWriterFinish(W);  /* проверить полноту пар и забрать результат */

Парность открытий и закрытий проверяет Finish — недостающий End вскрывается здесь, а не производит покалеченный JSON. Writer умеет выводить в память (Take владеющей строки) или писать прямо в sink (изоморфно системе sink-ов Logger-а главы 35) — второе позволяет сериализации напрямую соединяться с файлом или сетью.

Правила выбора маршрута

СценарийМаршрутПричина
Конфигурации/небольшие документыDOMпроизвольный доступ, кратчайший код
Однопроходный скан большого файлаSAXпамять не зависит от размера файла
Вывод по мере генерацииWriterбез промежуточного дерева
Изменить и пересериализоватьDOMправка на дереве, затем Stringify

Границы ресурсов и строгость

Три шлюза для недоверенного JSON (из xrtresourcelimits главы 3): глубина вложенности, число записей, суммарные байты — злонамеренный документ отвергается на 128-м уровне, не проедая стек насквозь. Дубликаты ключей отвергаются («одноимённый ключ перезаписывает» в конфигурационном файле — авария, а не свойство); разбор чисел идёт по строгой семантике главы 26. Конфигурация разбора различается по сторонам границы доверия: локально порождённым конфигурациям достаточно границ по умолчанию, внешнему входу ужесточайте все три шлюза.

32.3示例

Полная программа: три маршрута одним заходом

Из образца репозитория examples/data/json/main.c — одни и те же данные читаются DOM-ом, сканируются SAX-ом, пишутся Writer-ом:

examples/data/json/main.c源码 ↗





































 */

#include <stdio.h>

#include <xrt.h>







 */
static xjsonvisitaction printJsonEvent(
	const xjsonevent* pEvent,
	ptr pUserData
)
{
	(void)pUserData;
	if ( pEvent->HasName ) {
		printf("member: %.*s\n", (int)pEvent->Name.Size, pEvent->Name.Data);
	}
	return XJSON_VISIT_NEXT;
}



int main(void)
{
	xjsonreadconfig ReadConfig;
	xjsonwriteconfig WriteConfig;
	xjsonwriter* pWriter;
	xvalue* pRoot;
	xvalue* pName;
	xstrview Name;
	str sText;
	size_t iSize;

	/* ---- 1) DOM:解析 → 随机访问 → 美化序列化 ---- */
	pRoot = xrtJsonParse(XRT_STR_LITERAL(
		"{\"name\":\"xrt\",\"features\":[\"json\",\"http\"]}"
	));
	if ( pRoot == NULL ) {
		return 1;
	}

	/* ObjectGet 返回借用指针(树拥有它);GetString 取出视图。 */
	pName = xrtValueObjectGet(pRoot, XRT_STR_LITERAL("name"));
	if ( !xrtValueGetString(pName, &Name) ) {
		xrtValueRelease(pRoot);
		return 2;
	}
	printf("name = %.*s\n", (int)Name.Size, Name.Data);

	/* Stringify 产物是拥有式字符串(xrtFree);之后树即可释放。 */
	sText = xrtJsonStringify(pRoot, true, &iSize);
	xrtValueRelease(pRoot);
	if ( sText == NULL ) {
		return 3;
	}
	printf("%.*s\n", (int)iSize, sText);
	xrtFree(sText);

	/* ---- 2) SAX:同一份输入按事件流走一遍,不建树 ---- */
	xrtJsonReadConfigInit(&ReadConfig);
	if (
		xrtJsonVisit(
			XRT_STR_LITERAL("{\"code\":200,\"ok\":true}"),
			&ReadConfig,
			printJsonEvent,
			NULL
		) != XJSON_VISIT_DONE
	) {
		return 4;
	}

	/* ---- 3) Writer:增量构建 {"code":200,"message":"OK"} ---- */
	xrtJsonWriteConfigInit(&WriteConfig);
	pWriter = xrtJsonWriterCreate(&WriteConfig);
	if (
		(pWriter == NULL) ||
		!xrtJsonWriterObject(pWriter) ||                     /* 开对象 */
		!xrtJsonWriterName(pWriter, XRT_STR_LITERAL("code")) ||
		!xrtJsonWriterInt(pWriter, 200) ||
		!xrtJsonWriterName(pWriter, XRT_STR_LITERAL("message")) ||
		!xrtJsonWriterString(pWriter, XRT_STR_LITERAL("OK")) ||
		!xrtJsonWriterEnd(pWriter) ||                       /* 闭对象 */
		!xrtJsonWriterFinish(pWriter)                       /* 完整性校验 */
	) {
		xrtJsonWriterFree(pWriter);
		return 5;
	}

	/* Take 移交文本(xrtFree 释放);Take 后 writer 仍需 Free。 */
	sText = xrtJsonWriterTake(pWriter, &iSize);
	xrtJsonWriterFree(pWriter);
	if ( sText == NULL ) {
		return 6;
	}
	printf("%.*s\n", (int)iSize, sText);
	xrtFree(sText);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/data/json/main.c -lws2_32 -liphlpapiname = xrt{ "name": "xrt", "features": [ "json", "http" ]}member: codemember: ok{"code":200,"message":"OK"}

Что только что произошло. (1) Сегмент DOM: Parse даёт корневое значение, ObjectGet("name") берёт члена по имени (заимствование, не передача владения), GetString вычитывает представление и печатает — три шага, ежедневная форма чтения конфигурации. (2) Сегмент сериализации: Stringify(值, true) (значение, true) выводит красиво — формат с отступами для человека; межмашинный обмен — false (компактно), объём меньше. (3) Сегмент SAX: Visit вызывает обратный вызов событие за событием, выводится member: code/ok — каждое имя члена в потоке событий прошло через руки обратного вызова, промежуточного дерева нет от начала до конца. (4) Сегмент Writer — во второй половине файла: инкрементная сборка с проверкой Finish; последняя строка {"code":200,"message":"OK"} — продукт Writer-а; читая код, отметьте, что каждый Object/Name/Int/String сцеплен с End. (5) Пятая строка обзорного образца проверяет позицию ошибки: подайте вход без запятой — error-location line=1 укажет строку; ещё одно исполнение принципа «ошибка несёт свою позицию» из главы 4.

Полная программа: обход всех интерфейсов (файлы и потоки)

Из examples/data/json_tour/main.c — файловый разбор, файловая запись, потоковая запись и запись в sink:

examples/data/json_tour/main.c源码 ↗






















 */

#include <stdio.h>
#include <string.h>
#include <xrt.h>

#define SV(x) XRT_STR_LITERAL(x)

/* 收集回调:拼接全部分片。 */
typedef struct examplesink {
	char Buffer[128];
	size_t Size;
} examplesink;

static bool exampleCollect(xbytesview Data, ptr pUserData)
{
	examplesink* pSink = (examplesink*)pUserData;

	if ( (pSink->Size + Data.Size) < sizeof(pSink->Buffer) ) {
		memcpy(pSink->Buffer + pSink->Size, Data.Data,
			Data.Size);
	}
	pSink->Size = pSink->Size + Data.Size;
	return true;
}

int main(void)
{
	static const char sFile[] = "xrt-json-tour.json";
	static const char sText[] = "{\"a\":1,\"b\":[true,null]}";
	xjsonreadconfig ReadConfig;
	xjsonwriteconfig WriteConfig;
	xvalue* pDom = NULL;
	xvalue* pFileDom = NULL;
	xvalue* pReadDom = NULL;
	xvalue* pInner = NULL;
	xjsonwriter* pWriter = NULL;
	xerror* pError = NULL;
	xjsonlocation Location;
	examplesink Sink;
	size_t iWritten = 0;
	FILE* pOut;
	int iResult = 1;

	/* ---- Read(配置形态)+ Valid 正反 ---- */
	xrtJsonReadConfigInit(&ReadConfig);
	pDom = xrtJsonRead(SV(sText), &ReadConfig);
	if ( (pDom == NULL) ||
		!xrtJsonValid(SV(sText)) ||
		xrtJsonValid(SV("{\"a\":}")) ||
		!xrtJsonValid(SV("[1,2,3]")) ) {
		goto Cleanup;
	}
	/* ParseFile / ReadFile:先落盘再双形态读回。 */
	pOut = fopen(sFile, "wb");
	if ( (pOut == NULL) ||
		(fwrite(sText, 1u, sizeof(sText) - 1u, pOut) !=
			sizeof(sText) - 1u) ) {
		goto Cleanup;
	}
	fclose(pOut);
	pOut = NULL;
	pFileDom = xrtJsonParseFile(sFile);
	pReadDom = xrtJsonReadFile(sFile, &ReadConfig);
	if ( (pFileDom == NULL) || (pReadDom == NULL) ) {
		goto Cleanup;
	}
	printf("json: read+valid ok file-parse/read ok\n");

	/* ---- WriteFile / StringifyFile ---- */
	xrtJsonWriteConfigInit(&WriteConfig);
	if ( !xrtJsonWriteFile(sFile, pDom, &WriteConfig) ||
		((pInner = xrtJsonParseFile(sFile)) == NULL) ) {
		goto Cleanup;
	}
	xrtValueRelease(pInner);
	pInner = NULL;
	if ( !xrtJsonStringifyFile(sFile, pDom, true) ) {
		goto Cleanup;
	}
	{
		/* pretty 输出多行:读回验证仍是合法 JSON。 */
		pInner = xrtJsonParseFile(sFile);
		if ( pInner == NULL ) {
			goto Cleanup;
		}
		xrtValueRelease(pInner);
		pInner = NULL;
	}
	printf("json: write-file/stringify-file(pretty) ok\n");

	/* ---- Write 流式 + QuoteWrite ---- */
	Sink.Size = 0;
	if ( !xrtJsonWrite(pDom, &WriteConfig, exampleCollect, &Sink) ||
		(Sink.Size != sizeof(sText) - 1u) ||
		(memcmp(Sink.Buffer, sText,
			sizeof(sText) - 1u) != 0) ) {
		goto Cleanup;
	}
	printf("json: stream write=%zu", Sink.Size);
	Sink.Size = 0;
	if ( !xrtJsonQuoteWrite(SV("a\"b\\c"), 0u, exampleCollect,
			&Sink, &iWritten) ||
		(Sink.Size != 9u) ||  /* 带引号转义 "a\"b\\c" 共 9 字节 */
		(memcmp(Sink.Buffer, "\"a\\\"b\\\\c\"", 9u) != 0) ) {
		goto Cleanup;
	}
	printf(" quote=%zu\n", Sink.Size);

	/* ---- Sink 写入器:数组五元素 ---- */
	Sink.Size = 0;
	pWriter = xrtJsonWriterCreateSink(&WriteConfig, exampleCollect,
		&Sink);
	if ( (pWriter == NULL) ||
		!xrtJsonWriterArray(pWriter) ||
		!xrtJsonWriterUInt(pWriter, 1u) ||
		!xrtJsonWriterBool(pWriter, true) ||
		!xrtJsonWriterNull(pWriter) ||
		!xrtJsonWriterFloat(pWriter, 2.5) ) {
		goto Cleanup;
	}
	/* WriterValue:嵌套一个字符串 DOM。 */
	pInner = xrtValueString(SV("v"));
	if ( (pInner == NULL) ||
		!xrtJsonWriterValue(pWriter, pInner) ||
		!xrtJsonWriterEnd(pWriter) ||
		!xrtJsonWriterFinish(pWriter) ) {
		goto Cleanup;
	}
	if ( (Sink.Size != 21u) ||
		(memcmp(Sink.Buffer, "[1,true,null,2.5,\"v\"]",
			21u) != 0) ) {
		goto Cleanup;
	}
	printf("json: sink-writer [%.*s] ok\n",
		(int)(Sink.Size < 30u ? Sink.Size : 30u), Sink.Buffer);

	/* ---- ErrorLocation:截断文本的行列定位 ---- */
	{
		xvalue* pBad = xrtJsonRead(SV("{\"a\":"), &ReadConfig);

		if ( (pBad != NULL) ||
			((pError = xrtTakeError()) == NULL) ||
			!xrtJsonErrorLocation(pError, &Location) ||
			(Location.Line != 1u) ) {
			goto Cleanup;
		}
		xrtErrorFree(pError);
		pError = NULL;
	}
	printf("json: error-location line=%zu ok\n", Location.Line);
	iResult = 0;

Cleanup:
	xrtErrorFree(pError);
	xrtJsonWriterFree(pWriter);
	xrtValueRelease(pInner);
	xrtValueRelease(pReadDom);
	xrtValueRelease(pFileDom);
	xrtValueRelease(pDom);
	if ( pOut != NULL ) {
		fclose(pOut);
	}
	(void)remove(sFile);
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/data/json_tour/main.c -lws2_32 -liphlpapijson: read+valid ok file-parse/read okjson: write-file/stringify-file(pretty) okjson: stream write=23 quote=9json: sink-writer [[1,true,null,2.5,"v"]] okjson: error-location line=1 ok

Что только что произошло. (1) Файловые маршруты: Parse и Stringify работают с файлами напрямую — файловая форма загрузки и экспорта конфигурации. (2) Потоковая запись: Writer пишет элемент за элементом, счёт 23 элемента и 9 кавычек — эмпирическое доказательство «вывода по мере генерации». (3) Запись в sink: выход Writer-а подключается прямо к цели (буфер/файл) — сериализация в композиции с вводом-выводом, без промежуточной строки. Четыре позы покрывают все точки встречи JSON с файловой системой (глава 45).

32.4契约

  • Три маршрута: DOM — произвольный доступ / SAX — один проход без дерева / Writer — инкрементный вывод; выбор по форме данных, не по вкусу.
  • Строгость: дубликаты ключей отвергаются; числа по строгой семантике главы 26; ошибки со строкой и столбцом (xrtJsonErrorLocation).
  • Границы ресурсов: недоверенный вход — обязательные три шлюза (глубина/записи/байты); линия обороны на входе разбора, не в бизнес-слое.
  • Точность кругового прохода: сериализация плавающих — кратчайший круговой проход; контур чтения-записи без дрейфа.
  • Владение: корневое значение Parse освобождается целиком через Release; ObjectGet заимствует; продукт Stringify — xrtFree.
  • Парность Writer-а: открытия и закрытия проверяются Finish; вывод забираем (память) или подключаем напрямую к sink.

От примеров к инженерии: три хозяина JSON

Слой конфигурации: загрузка на старте → дерево DOM → система слияния главы 30 (умолчания + пользовательские переопределения) → только чтение на всём пути. Три дисциплины конфигурации: шлюзы ужесточаются по бизнесу (в конфигурации не бывает вложенности в десять тысяч уровней), отсутствующее поле и несовпадение типа — оба докладываются (точное чтение главы 30), при горячей перезагрузке старое дерево Release-ится, новое занимает место (счёт ссылок гарантирует идущим запросам спокойно дочитать).Слой протокола: разбор JSON-тела каждого запроса — SAX или DOM по объёму, границы ресурсов обязательны (внешний вход), позиция ошибки пишется в ответ 400, помогая клиенту самопровериться.Слой обмена данными: передача между модулями и через процессы — дерево значений напрямую (балансировка счёта ссылок, см. трёх хозяев главы 30) либо сериализация и путь через канал (кратчайший круговой проход гарантирует точность). Все три слоя на одном комплекте строгости; различаются лишь ширина шлюзов и адресат отчётов об ошибках.

Контраст: форма кода трёх маршрутов

Одна потребность — «взять поле имени пользователя» — в форме трёх маршрутов стоит прочитать бок о бок. DOM: Get(Get(Root,"user"),"name") — два прыжка, кратчайший доступ, повторяемый. SAX: в обратном вызове держится машина состояний «сейчас внутри объекта user», при событии name — запись; код длиннее, зато память константна. Writer: обратная задача (порождать, а не читать), Name+String за два шага — здесь нет «взять», есть только «дать». Прочитав три фрагмента рядом, вы получаете интуицию выбора маршрута: паттерн доступа диктует форму кода; неуклюжая форма обычно означает неверно выбранный маршрут.

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

Ловушка 1: внешний JSON без границ ресурсов:Ловушка 1: внешний JSON без границ ресурсов

Симптом: при обработке загруженного пользователем JSON — переполнение стека или взлёт памяти; текст с десятитысячу уровней вложенности проедает стек рекурсивного парсера насквозь.

Причина: границы по умолчанию мягкие (рассчитаны на доверенные конфигурации); внешнему входу нужны ужесточённые три шлюза — линия обороны на входе разбора.

错误写法
xvalue* pRoot = xrtJsonParse(Text, NULL);   /* границы по умолчанию - десятитысячу уровней сразу рвёт стек */
正确写法
xrtresourcelimits Limits;
xrtResourceLimitsInit(&Limits);
Limits.iMaxDepth = 64;      /* бизнесу нужно максимум 3 уровня; 64 - уже десятикратный запас */
xvalue* pRoot = xrtJsonParse(Text, &Limits);
if ( pRoot == NULL ) {
	size_t iLine, iColumn;
	xrtJsonErrorLocation(xrtGetError(), &iLine, &iColumn);
	Reject(iLine, iColumn);   /* превышение - отказ; и ошибка с позицией */
}
Ловушка 2: сохранение заимствованных указателей в обратных вызовах SAX:Ловушка 2: сохранение заимствованных указателей в обратных вызовах SAX

Симптом: указатели строк, сохранённые во время скана, используются после — читаются висячие или переиспользованные данные; та же семья, что ловушка 1 главы 18.

Причина: строки событий — заимствованные представления, срок действия кончается вместе с вызовом; парсер переиспользует свои буферы.

错误写法
static bool onEvent(const xjsonevent* pEvent, ptr pData)
{
	if ( pEvent->Type == XJSON_EVENT_STRING ) {
		gSaved = pEvent->String;   /* заимствованное представление в глобальную переменную - висячее к следующему вызову */
	}
	return true;
}
正确写法
static bool onEvent(const xjsonevent* pEvent, ptr pData)
{
	if ( pEvent->Type == XJSON_EVENT_STRING ) {
		Process(pEvent->String);   /* израсходовать в пределах жизни вызова */
		/* нужно сохранить: сперва Dup-ните копию */
	}
	return true;
}

Практические задания

Базовое: читатель конфигурации (трёхшаговая поза DOM)

Прочтите { "host": "0.0.0.0", "port": 8080, "debug": true } маршрутом DOM: возьмите три поля по имени и напечатайте (порт и выключатель — точным чтением главы 30).

Продвинутое: сканер JSON Lines

Просканируйте маршрут SAX файл .jsonl (по одному объекту JSON на строку): разбор построчно, подсчёт в обратном вызове событий каждого типа, поиск номеров строк с "level":"error". Подсказка: разделение на строки — это просто «один вызов Visit на строку»; сочетайте с построчной итерацией главы 25.

Вызов: потоковый генератор ответов

Реализуйте write_page(Writer, 页号, 数据数组) (Writer, номер страницы, массив данных): Writer инкрементно пишет объект постраничного ответа (метаданные + массив данных), элементы данных берутся из бизнес-структур (без сборки дерева значений); вывод подключён одновременно к памяти и к файловому sink. Критерии приёмки: произведённый JSON проходит проверку Parse (круг легален); генерация миллиона элементов пикует на памяти, не зависящей от числа элементов (доказательство потоковости); файловый и меморийный продукты совпадают байт в байт.

Итоги

ТемаКратко
Три маршрутаDOM (Parse/Get/Stringify) / SAX (поток событий Visit) / Writer (инкрементная сборка)
Правила выборапроизвольный доступ → DOM / однопроходный скан → SAX / вывод по мере генерации → Writer
Строгостьдубликаты ключей отвергаются; ошибка со строкой и столбцом (xrtJsonErrorLocation)
Три шлюзаглубина/записи/байты — обязательны для внешнего входа; превышение — отказ целиком с отчётом позиции
Плавающиесериализация кратчайшим круговым проходом — контур чтения-записи без дрейфа
Writerпарность открытий/закрытий проверяет Finish; Take забирает или подключение прямо к sink
ВладениеRelease корневого значения освобождает всё дерево; доступ к членам — заимствование; при горячей перезагрузке старое дерево безопасно уходит
Три хозяинаслой конфигурации (система слияния) / слой протокола (три шлюза + эхо ошибок) / слой обмена (дерево или сериализация)
Критерий формыпаттерн доступа диктует форму кода — неуклюжая форма обычно означает неверный маршрут

📖 Справочники API в этой главе:json (46 записей)value (159 записей)

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