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

Система типов (глава 114) описывает факты; объектный слой заставляет факты жить: runtime_object даёт счётные heap-объекты (xrtobject — непрозрачный контрольный блок + одно выделение с заголовком/выравниванием/нагрузкой) и слабые ссылки (xrtweak — повышаемая наблюдающая ссылка); runtime_object_graph добавляет сверху обрезаемый сбор сильных циклов — счёт ссылок не берёт циклические ссылки (A держит B, B держит A — счёт никогда не обнулится), поэтому граф объектов собирает недостижимые циклы двухпроходной разметкой (O(N+E)) в безопасных точках, выбираемых вызывающим. Проектная позиция: «сохранить преимущества счёта ссылок в детерминированном разрушении, владении C-расширений и слабых ссылках и обрабатывать только те циклы, которые чистый счёт вернуть не может, в выбираемых вызывающим безопасных точках» — не поколенческий GC, а точный патч. Контракт Trace (ведомое метаданными типа перечисление сильных ссылок) объединяет трассировку и разрушение на одних фактах (исполнение InstanceOps главы 114).

115.1引入

Оба конца жизненного цикла значений языка-хозяина вне власти C: момент исчезновения скриптовой переменной решает интерпретатор, а циклы между объектами разрешены семантикой языка (a.next = b; b.prev = a). Счёт ссылок берёт первое (переменная исчезла → Unref — детерминированное разрушение), но не второе (счёт внутри цикла вечно положителен). Три исторических ответа: полный поколенческий GC (stop-the-world, отложенное разрушение — кошмар владения для C-расширений); ручной weak_table (каждый рантайм языка переписывает свой); точный сбор циклов (только в безопасных точках, только циклы, остальное — счёту ссылок) — xruntime выбирает третий путь: граф лишь заимствует объекты, не держа постоянных ссылок, а при выключенной функции раскладка объекта имеет ноль полей графа (обрезка реальна — не платите ни копейки, если граф не нужен).

Дисциплина безопасных точек — ядро: сбор обязан назначаться верхним рантаймом, когда граф объектов неподвижен (задачи/Future/генераторы в стабильном состоянии) — XRT не притворяется, что произвольную нагрузку можно безопасно читать конкурентно под одним глобальным лок-ом; Trace/перечисление корней/Drop не должны запускать новые сборы. Это объектно-графовая версия структурированной конкурентности главы 60: согласованное наблюдение в остановленном мире.

115.2概念

Раскладка объекта и сильные ссылки

1СозданиеxrtObjectCreate(&Type) - одно выделение (заголовок + выравнивающий паддинг + нагрузка)
2Сильные ссылкиих дают Create/Lock; Ref наращивает (на время вызова действительная сильная ссылка уже должна быть - не воскрешать висячее)
3Доступ к нагрузкеObjectData/ConstData/Size заимствуют - действительны только при удержании сильной ссылки

CreateSized поддерживает реальные нагрузки не меньше InstanceSize (переменная хвостовая часть интерпретируется/уничтожается самим типом). Ответственность Init/Drop: провалившийся Init сам освобождает частичные ресурсы и ставит ошибку (система не зовёт Drop — Drop исполняется только для полностью инициализированных объектов).

Значения-объекты и слабые ссылки

Значения-объекты (слоты сильных ссылок, хранимые как обычные значения): объявите COPYABLE+RELOCATABLE и Ops = xrtObjectValueOps() — стандартные операции над значением-объектом поддерживают нулевую инициализацию/атомарную при отказе копию/перемещение/освобождение/хеш по адресу/трассировку сильных ссылок — слоты xrtobject* в полях, параметрах и элементах контейнеров обрабатываются ими единообразно (основа хранения объектов в typed-контейнерах главы 117). Слабые ссылки (xrtweak): маленькие значения на стеке/в структурах/в контейнерах — обнуляются до первого использования; Init (из опционального живого объекта)/Copy (заменяет цель, при отказе сохраняет прежнюю, копия в себя — пустая операция)/Expired (завершилась ли цель)/Lock (единственное повышение — успех возвращает новую сильную ссылку, завершённая цель возвращает пусто — гонка «проверил-использую» уничтожается атомарностью Lock).

Граф объектов: контракт трассировки

Собираемые типы через InstanceOps.Trace точно перечисляют сильные ссылки, которыми нагрузка владеет напрямую:

static bool nodeTrace(const void* Value, const xrttype* Type,
		xrtobjectvisitor Visit, ptr Context) {
	const node* Node = (const node*)Value;
	return (Node->Next == NULL) || Visit(Node->Next, Context);
}

Три железных правила: каждый действительный слот сильной ссылки посещается один раз (два поля с одним объектом — два посещения — семантика счёта); слабые ссылки/заимствованные указатели/пустые слоты не посещаются; Drop обязан освободить все сильные ссылки, о которых сообщил Trace — нарушение означает отказ сбора или утечку. Поля языка, элементы контейнеров, захваты замыканий должны переиспользовать одни и те же метаданные типа для порождения трассировки и разрушения (единственный источник фактов — философия главы 114, исполненная GC).

Сбор: двухпроходная разметка и корни

1Снимоквзять множество объектов графа (TrackedCount)
2Первый проходTrace всех - подсчёт внутренних рёбер (EdgeCount)
3Опознавание корнейсильных ссылок > внутренних рёбер = внешний корень; callback Roots добавляет заимствованные корни (стеки языка/генераторы/состояние хозяина)
4Второй проходраспространение достижимости от корней - O(N+E), временная память O(N)
5Финализациявсе недостижимые кандидаты одновременно получают права финализации -> Drop по одному -> расцепление - слабые ссылки с финализации не повышаются

Особая обработка оболочки Value: ссылки xvalue и COW-подложка контейнеров не наращивают поэлементно внутренние сильные ссылки — автоматического вывода корней мало; runtime_value_roots даёт CollectValueRoot/CollectValueRoots (одно значение / пакетно стек-слоты, глобальные слоты, подвешенные кадры) — переиспользуя xrtValueTraceRuntimeObjects, с завёрнутой при отказе цепочкой причин. Узлы динамических полей (xrtdynamicfields): отдельные словарные узлы в графе объектов — хозяин трассирует только объекты полей, а typed dict-нагруза полей трассирует ссылки значений через xrtTypeValue() — расслоение, не дающее сборщику рекурсивно гадать в словари xvalue.

Атомарность отказа и домены ошибок

Все предварительные проверки завершаются до любого разрушения (выделение/трассировка/перечисление корней/согласованность рёбер/проверки счёта снимка) — при любом отказе члены графа/сильные-слабые ссылки/нагрузки/выходы не меняются; расцепление происходит только когда все кандидаты одновременно получили права финализации — состояния «собрал половину» не существует. Домен ошибок xrt.object-graph — пять кодов (ARGUMENT/TRACK/TRACE/STATE/ROOTS); изоляция и восстановление ошибок потока имеют точные правила (успешное восстановление возвращает исходную ошибку вызывающего, отказ сохраняет только текущую).

115.3示例

Первая полная программа: сбор самоцикла

Программа ниже из examples/runtime/object_graph — построение и возврат циклической ссылки:

extlibs/xruntime/examples/runtime/object_graph/main.c源码 ↗
#include <stdio.h>
#include <xruntime.h>



typedef struct node {
	xrtobject* Next;
} node;



/* 释放节点拥有的后继引用。 */
static void nodeDrop(ptr pValue, const xrttype* pType)
{
	node* pNode = (node*)pValue;
	(void)pType;

	xrtObjectUnref(pNode->Next);
	pNode->Next = NULL;
}



/* 枚举节点直接拥有的后继引用。 */
static bool nodeTrace(
	const void* pValue,
	const xrttype* pType,
	xrtobjectvisitor pVisit,
	ptr pContext
)
{
	const node* pNode = (const node*)pValue;
	(void)pType;

	return (pNode->Next == NULL) || pVisit(pNode->Next, pContext);
}



/* 创建一个自引用对象,并在安全点回收它。 */
int main(void)
{
	static const xrtinstanceops Ops = {
		.Drop = nodeDrop,
		.Trace = nodeTrace
	};
	xrttype Type = {
		.Id = xrtTypeId(XRT_STR_LITERAL("example.Node")),
		.Kind = XRT_TYPE_CLASS,
		.Flags = XRT_TYPE_FLAG_REFERENCE | XRT_TYPE_FLAG_NULLABLE,
		.Name = XRT_STR_INIT("Node"),
		.AbiName = XRT_STR_INIT("example.Node"),
		.Size = sizeof(ptr),
		.Align = sizeof(ptr),
		.InstanceSize = sizeof(node),
		.InstanceAlign = sizeof(ptr),
		.InstanceOps = &Ops
	};
	xrtobjectgraphresult Result;
	xrtobjectgraph* pGraph = xrtObjectGraphCreate();
	xrtobject* pNode = xrtObjectCreate(&Type);

	if ( (pGraph == NULL) || (pNode == NULL) ||
		 !xrtObjectGraphTrack(pGraph, pNode) ) {
		xrtObjectUnref(pNode);
		xrtObjectGraphDestroy(pGraph);
		return 1;
	}
	((node*)xrtObjectData(pNode))->Next = xrtObjectRef(pNode);
	xrtObjectUnref(pNode);
	if ( !xrtObjectGraphCollect(pGraph, &Result) ) {
		xrtObjectGraphDestroy(pGraph);
		return 2;
	}
	printf("tracked=%zu edges=%zu collected=%zu\n",
		Result.TrackedCount, Result.EdgeCount, Result.CollectedCount);
	xrtObjectGraphDestroy(pGraph);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXRUNTIME_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xruntime.h impl.c extlibs/xruntime/examples/runtime/object_graph/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 tracked/edges/collected 的收集结果自检行)

Что только что произошло. (1) nodeDrop освобождает ссылку Next, nodeTrace перечисляет Next — симметрия Drop и Trace (железное правило три: Drop освобождает всё, о чём сообщил Trace). (2) Строится самоцикл: Node->Next = xrtObjectRef(Node), затем Unref внешней ссылки — в этот момент остаётся только самоподдержка цикла (счёт вечно положителен, внешних ссылок нет). (3) xrtObjectGraphCollect одним вызовом: снимок → двухпроходная разметка → опознавание «нет внешних корней» → финализация — CollectedCount сообщает число возврата. Это то, чего не может счёт ссылок и что граф делает одной фразой; а всё вне сбора (создание/доступ/обычное разрушение) остаётся детерминированным миром чистого счёта.

Вторая полная программа: жизненный цикл объекта и слабых ссылок

Вторая программа из examples/runtime/object — повседневная грань счёта ссылок:

extlibs/xruntime/examples/runtime/object/main.c源码 ↗
#include <stdio.h>
#include <xruntime.h>



int main(void)
{
	xrttype Type = {
		.Id = xrtTypeId(XRT_STR_LITERAL("example.Counter")),
		.Kind = XRT_TYPE_CLASS,
		.Flags = XRT_TYPE_FLAG_REFERENCE | XRT_TYPE_FLAG_NULLABLE,
		.Name = XRT_STR_INIT("Counter"),
		.AbiName = XRT_STR_INIT("example.Counter"),
		.Size = sizeof(ptr),
		.Align = sizeof(ptr),
		.InstanceSize = sizeof(int64),
		.InstanceAlign = sizeof(int64)
	};
	xrtweak Weak = { 0 };
	xrtobject* pCounter = xrtObjectCreate(&Type);

	if ( (pCounter == NULL) || !xrtWeakInit(&Weak, pCounter) ) {
		xrtObjectUnref(pCounter);
		return 1;
	}
	*(int64*)xrtObjectData(pCounter) = 42;
	printf("counter=%lld\n", (long long)*(int64*)xrtObjectData(pCounter));
	xrtObjectUnref(pCounter);
	printf("expired=%s\n", xrtWeakExpired(&Weak) ? "true" : "false");
	xrtWeakUnit(&Weak);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXRUNTIME_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xruntime.h impl.c extlibs/xruntime/examples/runtime/object/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 counter 值与弱引用 expired 状态的自检行)

Что только что произошло. (1) xrtObjectCreate + xrtObjectData пишут нагрузку — одно выделение, заимствованный доступ (при сильной ссылке). (2) Логика декремента счётчика идёт через InstanceOps (Init/Drop даёт тип — рассеянные ресурсы нагрузки чистятся в Drop). (3) Форма наблюдателя xrtWeakInit/Expired: после финализации объекта expired=true — слабая ссылка есть «наблюдение, не продлевающее жизнь» (стандартная деталь для кэшей/паттерна наблюдателя); для воскрешения нужен WeakLock — повышение до сильной (новая сильная ссылка есть только у живого объекта). Семья сопроводительных образцов: value_weak/value_roots (слабые ссылки и корни со стороны Value), value_trace (трассировка значений) — Value-грань экосистемы графа.

115.4契约

  • Раскладка объекта: одно выделение несёт заголовок/выравнивание/нагрузку; нагрузка удовлетворяет InstanceAlign (включая нестандартные 32/64B); провалившийся Init немедленно возвращается без Drop.
  • Дисциплина сильных ссылок: предусловие Ref — уже держать действительную (не воскрешать висячее); последний Unref Drop-ится в потоке-владельце; доступ к нагрузке только при сильной ссылке.
  • Значения-объекты: COPYABLE+RELOCATABLE+ObjectValueOps — единые операции для слотов полей/элементов/параметров; хеш по адресу только внутри процесса.
  • Слабые ссылки: обнуление при первом использовании; Lock — единственное атомарное повышение; с финализации не повышаются.
  • Обрезка графа: функция выключена = ноль полей графа, ноль кода сборщика; включена = объект принадлежит максимум одному графу, граф только заимствует, не наращивая ссылок.
  • Железные правила Trace: каждый слот сильной ссылки ровно один раз; слабые/заимствованные/пустые не посещаются; Drop освобождает полный набор Trace — нарушение = отказ или утечка.
  • Операции с членами: Track идемпотентен (принадлежность чужому графу → EXISTS); обычная финализация расцепляется автоматически; операции с членами одного графа можно конкурентно.
  • Безопасные точки: сбор при неподвижности; Trace/корни/Drop не должны входить в сбор повторно; никогда рядом с конкурентной модификацией той же партии объектов.
  • Атомарность отказа: все проверки впереди разрушения; кандидаты финализируются вместе; состояния «наполовину собрано» нет.
  • Сложность: двухпроходная разметка O(N+E), временная память O(N); четыре поля результата (Tracked/Edge/Root/Collected).
  • Корни Value: ссылки оболочки/COW требуют явных CollectValueRoot(s); динамические поля трассируются слоями.

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

Ловушка 1: Trace недосказывает сильные ссылки:Ловушка 1: Trace недосказывает сильные ссылки

Симптом: сбор отвергается (несогласованность входных рёбер) или объект течёт (надо было собрать, но не собрали) — Drop освободил ссылку, о которой Trace не сообщил.

Причина: «каждый слот ровно один раз» у Trace — фундамент корректности сбора: недосказанное искажает статистику рёбер (то, что было корнем, корнем не становится), пересказанное ложно осуждает достижимые объекты. Порождение Trace для полей языка обязано быть одного источника с раскладкой полей.

错误写法
static bool trace(const void* V, ..., xrtobjectvisitor Visit, ptr Ctx) {
	const node* N = V;
	return Visit(N->Next, Ctx);
	/* сильная ссылка N->Prev не сообщена - не хватает одного входного ребра: статистика сломана */
}
正确写法
static bool trace(const void* V, ..., xrtobjectvisitor Visit, ptr Ctx) {
	const node* N = V;
	if ( (N->Next != NULL) && !Visit(N->Next, Ctx) ) { return false; }
	return (N->Prev == NULL) || Visit(N->Prev, Ctx);  /* все сильные ссылки */
}
Ловушка 2: использование сырого указателя сразу после проверки Expired:Ловушка 2: использование сырого указателя сразу после проверки Expired

Симптом: в момент проверки жив, в момент использования завершён — TOCTOU-падение.

Причина: Expired — мгновенное наблюдение; единственно безопасное «проверить + использовать» — атомарное повышение WeakLock: успех означает новую сильную ссылку (объект обязан жить, пока она держится).

错误写法
if ( !xrtWeakExpired(&Weak) ) {
	use(xrtObjectData(Weak...));   /* между проверкой и использованием может завершиться */
}
正确写法
xrtobject* Strong = xrtWeakLock(&Weak);
if ( Strong != NULL ) {
	use(xrtObjectData(Strong));   /* держим сильную ссылку: жизнь гарантирована */
	xrtObjectUnref(Strong);
}
Ловушка 3: запуск сбора вне безопасной точки:Ловушка 3: запуск сбора вне безопасной точки

Симптом: сбор читает конкурентно изменяемые поля ссылок — ошибка STATE или, хуже, ошибочный сбор.

Причина: контракт прямо говорит «собирать при неподвижности» — Trace/запись полей/изменения счёта ссылок — всё модификации. Планировщик хозяина обязан входить в безопасную точку после стабилизации задач/Future/генераторов (семантика паузы главы 60).

错误写法
/* пока любой поток ещё меняет поля объектов */
xrtObjectGraphCollect(Graph, &Result);   /* риск STATE-ошибки / ошибочного сбора */
正确写法
/* хозяин входит в безопасную точку: задачи/генераторы на паузе, состояние стабильно */
host_suspend_all();
xrtObjectGraphCollect(Graph, &Result);
host_resume_all();

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

Базовое: эксперименты со счётом сильных и слабых ссылок

Объект + две сильные ссылки + одна слабая: пошаговый Unref с наблюдением точки переворота Expired; сравнение до и после повышения Lock. Критерии приёмки: точка финализации приходится ровно на последнюю сильную ссылку; слабая ссылка не продлевает жизнь; после неудачного Lock слабая ссылка переиспользуема (Init на новую цель).

Продвинутое: сбор двустороннего цикла

Двусторонний цикл A↔B + внешняя ссылка C→A: проверить, что при внешнем корне сбора нет (после освобождения C сбор успешен). Критерии приёмки: счётчики Root/Collected двух сборов сходятся с ручным выводом.

Вызов: интеграция безопасных точек хозяина

Сымитируйте планировщик хозяина: N «задач» держат ссылки на объекты — приостановить все (безопасная точка) → собрать → возобновить; «генератор», подвешенный среди задач, держит ссылки, добавляемые через callback Roots. Критерии приёмки: объекты под ссылками задач не собираются; после освобождения собираются; заимствованные корни callback-а живут до конца сбора; ошибок STATE ноль.

Итоги

ТемаКратко
Позиционированиепатч сбора циклов поверх счёта ссылок — не поколенческий GC; обрезка реальна (выкл = ноль цены)
Раскладка объектаодно выделение; нагрузка удовлетворяет InstanceAlign; провал Init — без Drop
Сильные ссылкиRef требует уже держать действительную; последний Unref Drop-ится в потоке-владельце; доступ только при удержании
Значения-объектыObjectValueOps — едино для слотов полей/элементов; сравнение по адресу только в процессе
Слабые ссылкиLock — единственное атомарное повышение; Expired — только наблюдение; с финализации не повышается
Железные правила Traceкаждый слот сильной ссылки ровно один раз; слабые/заимствованные/пустые не посещаются; Drop = полный набор Trace
Алгоритм сбораснимок → подсчёт рёбер → опознавание корней (авто + callback) → распространение достижимости O(N+E)
Безопасные точкисбор при неподвижности; callback-и не входят повторно; назначает планировщик хозяина
Атомарность отказапроверки впереди разрушения; кандидаты финализируются вместе; домен xrt.object-graph — пять кодов
Корни Valueоболочка/COW требуют явных ValueRoot(s); динамические поля трассируются слоями

📖 Справочники API в этой главе:xruntime-runtime_object (18 записей)xruntime-runtime_object_graph (8 записей)

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