Ориентация главы
Система типов (глава 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概念
Раскладка объекта и сильные ссылки
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).
Сбор: двухпроходная разметка и корни
Особая обработка оболочки 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 — построение и возврат циклической ссылки:
#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; }
Что только что произошло. (1) nodeDrop освобождает ссылку Next, nodeTrace перечисляет Next — симметрия Drop и Trace (железное правило три: Drop освобождает всё, о чём сообщил Trace). (2) Строится самоцикл: Node->Next = xrtObjectRef(Node), затем Unref внешней ссылки — в этот момент остаётся только самоподдержка цикла (счёт вечно положителен, внешних ссылок нет). (3) xrtObjectGraphCollect одним вызовом: снимок → двухпроходная разметка → опознавание «нет внешних корней» → финализация — CollectedCount сообщает число возврата. Это то, чего не может счёт ссылок и что граф делает одной фразой; а всё вне сбора (создание/доступ/обычное разрушение) остаётся детерминированным миром чистого счёта.
Вторая полная программа: жизненный цикл объекта и слабых ссылок
Вторая программа из examples/runtime/object — повседневная грань счёта ссылок:
#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; }
Что только что произошло. (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); динамические поля трассируются слоями.
Типичные ошибки
Симптом: сбор отвергается (несогласованность входных рёбер) или объект течёт (надо было собрать, но не собрали) — 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); /* все сильные ссылки */ }
Симптом: в момент проверки жив, в момент использования завершён — TOCTOU-падение.
Причина: Expired — мгновенное наблюдение; единственно безопасное «проверить + использовать» — атомарное повышение WeakLock: успех означает новую сильную ссылку (объект обязан жить, пока она держится).
if ( !xrtWeakExpired(&Weak) ) { use(xrtObjectData(Weak...)); /* между проверкой и использованием может завершиться */ }
xrtobject* Strong = xrtWeakLock(&Weak); if ( Strong != NULL ) { use(xrtObjectData(Strong)); /* держим сильную ссылку: жизнь гарантирована */ xrtObjectUnref(Strong); }
Симптом: сбор читает конкурентно изменяемые поля ссылок — ошибка 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки