(эксперимент десятилетней давности, который до сих пор не даёт мне покоя)
⚠️ Дисклеймер
Это не готовая библиотека и не «лучше protobuf».
Это экспериментальная система сериализации, написанная более 10 лет назад,
для решения задачи, с которой стандартные форматы справляются плохо:
сохранение и восстановление runtime-графов объектов с рефлексией и циклами.
Зачем вообще ещё одна сериализация?
сценарий:
У меня есть:
– редактор
– сцена
– объекты с взаимными ссылками
– UI, который указывает на поля этих объектовЯ хочу:
– сохранить всё
– поменять код
– загрузить обратно
– и не потерять ни одной связи
Большинство форматов сериализации (JSON, XML, protobuf, flatbuffers):
хорошо работают с деревьями
плохо работают с графами
-
почти не работают с:
циклами
указателями
полиморфизмом
ссылками на части объектов
А если работают — то ценой:
внешнего кода
жёсткой схемы
ручного glue-кода
Мне же нужна была система, которая умеет:
сериализовать произвольный объектный граф
сохранять типы и связи
быть самоописывающейся
и при этом иметь бинарный и текстовый режим
Так родилась эта система.
Ограничения (сразу и честно)
Чтобы сразу снять лишние вопросы:
компилируется только MSVC
только 32-bit
активно использует макросы
проект заморожен
время компиляции стало критической проблемой при росте кода
Это не «продакшн-решение», а инженерный эксперимент.
Общая идея формата
Файл сериализации состоит из четырёх логических частей:
Имена описателей метатипов
Описание всех используемых типов используя описатели метатипы
Что-то вроде карты связывающей описание типов с их реализацией
Собственно данные пользовательской структуры (объектный граф)
Причём:
файл почти не требует внешнего кода
он содержит почти всё необходимое для интерпретации данных
текстовый формат изоморфен бинарному
Два слоя описания типов: runtime и save-time
┌──────────────┐
│ C++ code │
│ (ABI, vtbl) │
└──────┬───────┘
│
▼
┌──────────────┐
│ RTTI │ ← фабрики, offset’ы, указатели
└──────┬───────┘
│ сопоставление
▼
┌──────────────┐
│ STTI │ │ ← имена, типы, поля
└──────┬───────┘
│ запись
▼
┌──────────────┐
│ .qap file │
└──────────────┘
Два слоя описания типов: runtime и save-time
Важно подчеркнуть одну архитектурную деталь, без которой вся система просто не работала бы.
На самом деле в системе две независимые системы описания типов:
1. Run-Time-Type-Info (исполняемая)
То, что используется во время работы программы:
фабрики объектов
смещения полей
реальные указатели
виртуальные таблицы
всё, что зависит от ABI, компилятора и layout’а памяти
Эта часть нужна, чтобы:
создавать объекты
ходить по ним
связывать указатели
вызывать код
2. Save-Time-Type-Info (декларативная)
Отдельный, более «чистый» слой, который используется только для сериализации:
описателей метатипов
описателей типов
Именно этот слой записывается в файл.
Ключевая идея в том, что:
файл сериализации не знает почти ничего о runtime-реализации,
он знает только декларативное описание структуры данных.
Благодаря такому разделению:
формат переживает изменение layout’а памяти
порядок полей не критичен
добавление/удаление полей возможно
миграция работает
По сути:
RTTI — это «как работать»
STTI — это «что именно сохранено»
И эти два мира намеренно не смешиваются.
Это решение:
резко усложнило код
увеличило объём метаописаний
потребовало поддержки двух систем типов
Но без него:
самоописание формата было бы невозможно
миграция между форматами не работала бы
Простейший пример: граф с циклами
Начнём с минимального примера.
Структуры
// https://github.com/adler3d/QapSerialize/blob/e55922ba0985587fb8cb0b9e89a97551ad8fce20/QapApp/CommonUnit.cpp#L18
class t_node{
public:
TSelfPtr<t_node> Self;
string name;
vector<t_node> arr;
vector<TWeakPtr<t_node>> links;
};
class t_some_class{
public:
t_node n;
int b = 10;
double c = 20;
string more = "2025.12.03 18:07";
double z = 30;
};
Ключевые моменты:
TSelfPtr— самоссылка (идентификатор объекта)TWeakPtr— слабая ссылка на другой объектvector<t_node>— вложенные объекты, а не указатели
Это уже не дерево, а граф.
Создание связей
t_some_class var;
var.n.links.push_back(&var.n);
var.n.arr.push_back(t_node());
var.n.arr.push_back(t_node());
var.n.arr.push_back(t_node());
var.n.links.push_back(&var.n.arr[2]);
var.n.links.push_back(&var.n.arr[1]);
var.n.links.push_back(&var.n.arr[0]);
var.n.arr[1].links.push_back(&var.n.arr[2]);
Здесь есть:
самоссылка
ссылки между вложенными объектами
циклы
Большинство сериализаторов на этом месте либо падают, либо требуют ручной работы.
Текстовый proto-файл
Помимо бинарного файла, система сохраняет текстовый дамп:
t_some_class
{
n = t_node
{
Self = TSelfPtr<t_node>(userpoint[0])
arr = vector<t_node>
{
t_node { Self = TSelfPtr<t_node>(userpoint[1]) }
t_node { Self = TSelfPtr<t_node>(userpoint[2]) }
t_node { Self = TSelfPtr<t_node>(userpoint[3]) }
}
links = vector<TWeakPtr<t_node>>
{
TWeakPtr<t_node>(userpoint[0])
TWeakPtr<t_node>(userpoint[3])
TWeakPtr<t_node>(userpoint[2])
TWeakPtr<t_node>(userpoint[1])
}
}
}
(Файл на самом деле больше — здесь только переработанная ИИ payload-часть показывающая как хранятся умные следящие указатели.)
Как выглядит описание типа в сохранении:
TAutoPtr<DeclareType>
{
DeclareTypeStruct
{
DeclareType{Self = TSelfPtr<DeclareType>(Def$$<t_node>::GetRTTI())}
Name = string("t_node")
SubType = TWeakPtr<DeclareType>(nullptr)
OwnType = TWeakPtr<DeclareType>(nullptr)
Members = vector<DeclareMember>{
DeclareMember{
Name = string("Self")
Type = TWeakPtr<DeclareType>(Def$$<TSelfPtr<t_node>>::GetRTTI())
Mode = string("DEF")
Value = string("$")
},
DeclareMember{
Name = string("name")
Type = TWeakPtr<DeclareType>(Def$$<string>::GetRTTI())
Mode = string("DEF")
Value = string("$")
},
DeclareMember{
Name = string("arr")
Type = TWeakPtr<DeclareType>(Def$$<vector<t_node>>::GetRTTI())
Mode = string("DEF")
Value = string("$")
},
DeclareMember{
Name = string("links")
Type = TWeakPtr<DeclareType>(Def$$<vector<TWeakPtr<t_node>>>::GetRTTI())
Mode = string("DEF")
Value = string("$")
}
}
}
}
Что такое userpoint[n]?
Это идентификатор объекта в графе.
каждый объект с
TSelfPtrполучает уникальный idTWeakPtrссылается на этот idпорядок создания не важен
циклы не проблема
Почему текстовый формат не предназн��чен для загрузки?
Это принципиальный момент.
Текстовый файл:
изоморфен бинарному
полностью повторяет структуру данных
-
предназначен для:
отладки
анализа
diff’ов
понимания состояния системы
Но:
он не устойчив к правкам руками
числа с плавающей точкой — отдельная боль
он ближе к IR, чем к DSL
Это осознанное решение.
Самоописание типов
До payload в файле содержится полное описание всех типов:
stringintrealt_nodevector<t_node>vector<TWeakPtr<t_node>>и т.д.
Файл можно читать без исходников.
Это сильно увеличивает размер, но даёт:
независимость
воспроизводимость
возможность анализа вне программы
Почему это вообще имеет смысл?
Потому что такой формат:
подходит для редакторов
позволяет сохранять состояние движка
удобен для live-reload
позволяет делать undo/redo как снапшоты
честно работает с графами
Это не замена JSON. Это решение другой задачи.
Часть 2. Полиморфизм, владение и ссылки на поля
В первом примере у нас был относительно простой граф:
один тип узла, вложенные объекты и слабые ссылки.
Теперь усложним задачу и посмотрим, где формат начинает реально отличаться от обычных сериализаторов.
Базовый интерфейс и наследники
Вводим наш пользовательский базовый тип:
class i_node{
public:
TSelfPtr<i_node> Self;
};
И два его наследника:
class t_node2 : public i_node{
public:
string name;
vector<TAutoPtr<i_node>> arr;
vector<TWeakPtr<i_node>> links;
};
class t_leaf : public i_node{
public:
string name;
string payload;
TFieldPtr fieldptr;
};
Ключевые отличия от первого примера:
vector<TAutoPtr<i_node>>— владение полиморфными объектамиvector<TWeakPtr<i_node>>— ссылки на базовый типt_leaf— отдельный тип узлаTFieldPtr— ссылка не на объект, а на его поле
Контейнер с полиморфизмом
vector<TAutoPtr<i_node>> arr;
Это означает:
контейнер знает только базовый тип
фактический тип элемента определяется в runtime
сериализация должна сохранить реальный тип объекта
В текстовом proto это выглядит так:
TAutoPtr<i_node>
{
t_leaf
{
...
}
}
Или:
TAutoPtr<i_node>
{
t_node2
{
...
}
}
Тип не угадывается и не «восстанавливается магией».
Он явно записан в данных(в бинаре как int равный SaveID из TSelfPtr целевого типа).
Как выглядит наследование в proto
Объект-наследник сериализуется как:
t_node2
{
i_node
{
Self = TSelfPtr<i_node>(userpoint[1])
}
name = string("N1")
arr = ...
links = ...
}
Важно:
базовая часть (
i_node) — реальный вложенный объектпорядок фиксирован
никаких неявных offset’ов или ABI-зависимостей
Это ближе к «снимку структуры», чем к классическому сериализованному объекту.
Граф остаётся графом
Несмотря на полиморфизм и владение:
TSelfPtrпродолжает идентифицировать объектTWeakPtrпродолжает ссылаться по idциклы допустимы
типы могут быть разными
Ссылки на поля: TFieldPtr
Теперь самое интересное (и самое спорное).
В t_leaf есть поле:
TFieldPtr fieldptr;
Оно может указывать, например, на конкретное поле конкретного объекта:
var.n.arr[1].build<t_leaf>(Env)->fieldptr.ConnectToField(Env,N1,N1.links);
То есть:
есть объект
N1у него есть поле
linkst_leafхранит ссылку на это поле
Как это выглядит в proto
fieldptr = TFieldPtr
{
object = TVoidPtr
{
type = THardPtr<TType>(Sys$$<t_node2>::GetRTTI())
ptr = (userpoint[1])
}
type = THardPtr<TType>(Sys$$<vector<TWeakPtr<i_node>>>::GetRTTI())
index = int(5)
}
Фактически здесь зафиксировано:
объект (
userpoint[1])тип объекта
тип поля
индекс поля в рефлексивном списке
Это уже рефлексия второго порядка.
Почему это впечатляет…
Потому что формат умеет:
сериализовать полиморфные графы
сохранять владение и ссылки
указывать не только на объект, но и на его часть
Это уровень:
визуальных редакторов
node-based систем
live graph editors
…и почему это оказалось тупиком
TFieldPtr оказался слишком хрупким.
Проблемы:
поле идентифицируется индексом
изменение порядка полей ломает семантику
добавление/удаление поля — риск
текстовый формат становится слишком «исполнительным»
По сути, это сериализация ABI рефлекс��и, а не структуры данных.
// если в целевом объекте добавить поле и индексы поедут, то будет примерно такая ошибка заргузки:
cannot convert from
'int t_node2::*'
to
'vector<TWeakPtr<i_node>> t_node2::*'
Часть 3. Что пошло не так, и какие идеи выжили
Эта часть — самая важная. Потому что именно здесь появляется опыт.
Проблема времени компиляции
Система активно использует:
шаблоны
макросы
RTTI
регистрацию типов
На маленьких тестах:
всё компилируется быстро
система ощущается удобной
Но при росте проекта:
время компиляции становится критичным
любая правка тянет за собой полмира
В какой-то момент это стало неприемлемо.
Проект был заморожен.
Миграция форматов (и это сработало)
При этом один важный эксперимент удался.
Были тесты, где:
менялся порядок полей
поля добавлялись
поля удалялись
И при этом:
старые файлы продолжали грузиться
данные не «ломались»
Почему?
сериализация опирается не на layout памяти
а на декларативное описание структуры
Это был сильный аргумент в пользу подхода.
Отказ от TFieldPtr
Со временем стало понятно:
ссылка на поле по индексу — плохая идея
она слишком низкоуровневая
Вместо этого родилась другая концепция.

TBranch: структурный путь
Идея:
хранить не «указатель»
Примерно как:
root.n.arr[1].links
Но:
в структурированном виде
с валидацией
с возможностью переинтерпретации
Плюсы:
устойчиво к изменению layout’а
идеально для текста
можно добраться до любого поля
Фактически — DSL навигации по объектному графу.
Про «умные слабые следящие указатели»
Самый неожиданный вывод:
Я так и не понял, как получать профит от умных слабых следящих указателей в реальном мире.
В прочем, как позднее выяснялось, проблема не в указателях.
На практике удобнее оказались:
индексы
строки
структурные пути
скрипты-указатели
Умные указатели хороши внутри кода, но в данных они часто создают больше проблем, чем решают.
Итоговый вывод
Эта система:
не стала библиотекой
не дошла до продакшна
но дала очень ценный опыт
Главный вывод от ИИ:
Сериализация — это не про байты. Это про сохранение инвариантов связей.
И чем сложнее система, тем важнее:
граф, а не дерево
семантика, а не layout
структура, а не адреса
Вместо заключения
Если бы я делал это сегодня:
меньше шаблонов
меньше магии
больше декларативности
больше текстовых, стабильных идентификаторов
Но идеи:
самоописания
графов
структурных путей
— я бы точно взял с собой дальше.
Часть 4. Жёсткие ограничения, которые всплыли на практике
На бумаге система выглядела мощной: графы, ссылки, полиморфизм, самоописание, миграции. Но при реальной попытке удалять поля и типы при миграции вскрылись неприятные ограничения. И это важнее любых красивых примеров.
Удаление объектов с TSelfPtr — неожиданная проблема
Первый тревожный симптом:
Удаление(при миграции) объекта с
TSelfPtrломает загрузку,
даже если на этот объект больше никто не ссылается.
Интуитивно ожидаешь:
нет
TWeakPtrнет
TAutoPtrобъект просто исчезает при миграции
Но нет.
Система не справляется с этим сценарием.
«А если есть слабая ссылка наружу?» — тоже нет
Следующая гипотеза была такой:
Если из удаляемого объекта есть только слабая ссылка наружу, значит его можно безопасно удалить.
Результат:
наличие
TSelfPtrв удаляемом при миграции типе пока делает миграцию невозможной
TAutoPtr усугубляет ситуацию
Следующее наблюдение оказалось ещё жёстче:
TAutoPtrвнутри удаляемого при миграции объекта хранить нельзя вообще
То есть если тип содержит:
TSelfPtrTAutoPtr
— система миграции пока не справляется.
Что в итоге реально безопасно мигрирует
После серии экспериментов стало ясно:
Надёжно удаляются при миграции только «тупые» типы
А именно:
встроенные типы
строки
структуры
векторы
комбинации вышеперечисленного
Без:
TSelfPtrTWeakPtrTAutoPtrTFieldPtr
Рабочий миграционный тест

Полный код теста:
class i_node{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(i_node)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(TSelfPtr<SelfClass>,Self,DEF,$,$)\
ADDEND()
//=====+>>>>>i_node
#include "QapGenStruct.inl"
//<<<<<+=====i_node
public:
virtual void check(){QapNoWay();}
};
class t_node2:public i_node{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_node2)PARENT(i_node)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(string,name,DEF,$,$)\
ADDVAR(vector<TAutoPtr<i_node>>,arr,DEF,$,$)\
ADDVAR(vector<TWeakPtr<i_node>>,links,DEF,$,$)\
ADDEND()
//=====+>>>>>t_some_class
#include "QapGenStruct.inl"
//<<<<<+=====t_some_class
public:
};
class t_leaf:public i_node{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_leaf)PARENT(i_node)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(string,name,DEF,$,$)\
ADDVAR(string,payload,DEF,$,$)\
ADDVAR(TFieldPtr,fieldptr,DEF,$,$)\
ADDEND()
//=====+>>>>>t_leaf
#include "QapGenStruct.inl"
//<<<<<+=====t_leaf
public:
void check()override{
QapAssert(name=="leaf");
QapAssert(payload=="foo");
}
};
class t_some_class2{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_some_class2)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_node2,n,DEF,$,$)\
ADDVAR(double,z,SET,30,$)\
ADDEND()
//=====+>>>>>t_some_class2
#include "QapGenStruct.inl"
//<<<<<+=====t_some_class2
public:
};
namespace t_before{
class t_old_stuff{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_old_stuff)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(string,name,DEF,$,$)\
ADDVAR(vector<t_old_stuff>,arr,DEF,$,$)\
ADDEND()
//=====+>>>>>t_old_stuff
#include "QapGenStruct.inl"
//<<<<<+=====t_old_stuff
};
class t_node2:public i_node{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_node2)PARENT(i_node)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(string,name,DEF,$,$)\
ADDVAR(vector<TAutoPtr<i_node>>,arr,DEF,$,$)\
ADDVAR(vector<TWeakPtr<i_node>>,links,DEF,$,$)\
ADDEND()
//=====+>>>>>t_node2
#include "QapGenStruct.inl"
//<<<<<+=====t_node2
public:
t_node2*init(const string&Name){name=Name;return this;}
};
class t_leaf:public i_node{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_leaf)PARENT(i_node)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(t_old_stuff,old_stuff,DEF,$,$)\
ADDVAR(string,payload,DEF,$,$)\
ADDVAR(string,payload2,DEF,$,$)\
ADDVAR(TFieldPtr,fieldptr,DEF,$,$)\
ADDVAR(string,payload3,DEF,$,$)\
ADDVAR(string,name,DEF,$,$)\
ADDEND()
//=====+>>>>>t_leaf
#include "QapGenStruct.inl"
//<<<<<+=====t_leaf
public:
t_leaf*init(IEnvRTTI&Env){
payload="foo";
payload2="bar";
payload3="baz";
name="leaf";
qap_add_back(qap_add_back(old_stuff.arr).arr).name="inner";
qap_add_back(old_stuff.arr).name="object";
old_stuff.name="stuff";
return this;
}
};
class t_some_class2{
public:
#define DEF_PRO_STRUCT_INFO(NAME,PARENT,OWNER)NAME(t_some_class2)
#define DEF_PRO_VARIABLE(ADDBEG,ADDVAR,ADDEND)\
ADDBEG()\
ADDVAR(double,z,SET,360,$)\
ADDVAR(t_node2,n,DEF,$,$)\
ADDVAR(double,x,SET,30,$)\
ADDEND()
//=====+>>>>>t_some_class2
#include "QapGenStruct.inl"
//<<<<<+=====t_some_class2
public:
};
void init(IEnvRTTI&Env,t_some_class2&var){
var.n.links.push_back(&var.n);
i_node*ptr=nullptr;
var.n.arr.push_back({});auto&N1=*var.n.arr[0].build<t_node2>(Env);N1.name="N1";
var.n.arr.push_back({});var.n.arr[1].build<t_leaf>(Env)
->init(Env)->fieldptr.ConnectToField(Env,N1,N1.links);
var.n.arr.push_back({});var.n.arr[2].build<t_node2>(Env)->name="N2";
var.n.links.push_back(var.n.arr[2].get());
var.n.links.push_back(var.n.arr[1].get());
var.n.links.push_back(var.n.arr[0].get());
N1.links.push_back(var.n.arr[2].get());
}
};
void main_2025(IEnvRTTI&Env){
{
// кто бы мог подумать что я не зря сделал TEnvRTTI не singleton`ом
// и поэтому теперь могу делать миграционный тест вот так:
TStdAllocator MA;
TEnvRTTI Env2;
Env2.Arr.reserve(1024);
Env2.Alloc=&MA;
Env2.OwnerEnv=&Env2;
t_before::t_some_class2 var;
t_before::init(Env2,var);
TQapFileStream fsproto("out_before.qap.proto",false);
QapUberFullSaver(Env2,QapRawUberObject(var),UberSaveDeviceProto(fsproto));
TQapFileStream fs("out.qap",false);
QapUberFullSaver(Env2,QapRawUberObject(var),UberSaveDeviceBin(fs));
}
Sys$$<t_node2>::GetRTTI(Env);Sys$$<t_leaf>::GetRTTI(Env);
t_some_class2 var;
if(!QapPublicUberFullLoaderBinLastHope(Env,QapRawUberObject(var),"out.qap")){
int gg=1;
}
var.n.arr[1]->check();
QapAssert(var.z==360);
TQapFileStream fsproto("out.qap.proto",false);
QapUberFullSaver(Env,QapRawUberObject(var),UberSaveDeviceProto(fsproto));
TQapFileStream fs("out.qap",false);
QapUberFullSaver(Env,QapRawUberObject(var),UberSaveDeviceBin(fs));
int gg=1;
}Суть теста миграции
Тест делает следующее:
Есть старая версия типов (
t_before::)-
В ней:
лишние поля
изменённый порядок
вложенные структуры
TFieldPtr Данные сохраняются в файл
Затем файл загружается в новую версию типов
-
Проверяется:
данные на месте
логика не сломалась
Что именно меняется между версиями
В старой версии t_leaf содержит:
old_stuff(вложенная структура)payloadpayload2payload3nameTFieldPtr
В новой версии t_leaf:
нет
old_stuffнет
payload2,payload3поля в другом порядке
-
остались только:
namepayloadTFieldPtr
При этом:
данные успешно загружаются
payload == "foo"name == "leaf"check()проходит
Этот тест работает, т.к:
сериализация опирается на имена полей
лишние поля просто игнорируются
отсутствующие поля получают дефолты
layout памяти не используется
данные сопоставляются декларативно

Что показывает diff proto-файлов
Diff между out_before.qap.proto и out.qap.proto наглядно показывает:
какие поля исчезли
какие типы больше не используются
как изменилось описание структуры
Где посмотреть/попробовать?
Репозиторий: Adler3d/QapSerialize
Код примера из статьи: CommonUnit.cpp
PS: Мечтаю сделать компилятор который будет сразу эффективно поддерживать такую систему сериализации, но пока на это нет ресурсов, может они есть у вас? Давайте построим крутую систему вместе :)
Комментарии (20)

domix32
15.12.2025 08:49Как-то вы на примеры сериализации/десериализации поскупились. Кажется проблема решается несколько проще без необходимости изобретать собственные умные указатели.

Adler3D Автор
15.12.2025 08:49Как-то вы на примеры сериализации/десериализации поскупились
Спасибо за замечание, хорошо, попробую добавить ещё примеров как только придумаю их. Есть какие-то пожелания что именно вы хотели бы увидеть в них?
Кажется проблема решается несколько проще без необходимости изобретать собственные умные указатели.
Не-а, похоже без своих умных указателей к сожалению невозможно вывести весь необходимый список типов который нужно добавить в файл сохранения для саммоописания формата сохранения.

domix32
15.12.2025 08:49Есть какие-то пожелания что именно вы хотели бы увидеть в них?
Вы же решали какую-то проблему. Уменьшите скоуп проблемы и сделайте из неё пример поменьше. Заодно будет видно зачем понадобилось иметь ссылочные типы и частичную сериализацию структур. Накидайте какой-нибудь граф, который вы хотели сериализовать.

Adler3D Автор
15.12.2025 08:49Вы же решали какую-то проблему
Я на основе этой системы сохранял/загружал/мигрировал всё состояние своих программ, а так же делал довольно крутой(
как мне тогда казалось)графический отладчик/интроспектор для них. Последний позволял добраться до почти любой хранимой переменной и поменять её.Уменьшите скоуп проблемы и сделайте из неё пример поменьше.
Проблема было две: "сохранить всё состояние программы" и "менять его в реальном времени не перезапуская программу", как мне уменьшить такой скоуп? Сохранить половину состояния программы? :)
Заодно будет видно зачем понадобилось иметь ссылочные типы и частичную сериализацию структур
Ссылочные типы очень пригодились для описания типов используемых внутри сохраняемого файла, хотя конечно всё можно было заменить на строки и тогда мы бы поменяли производительность на простоту реализации. У меня получилось нифига не просто, но зато компактно и быстро. Ещё ссылочные типы используются для указания на текстуры/шрифты/спрайты_в_атлассе/клавиатур... но всё это можно переделать на строковые идентификаторы и победить в 80% случаев как мне кажется. Вообще я планировал использовать ссылочные типы для того чтобы боты в игре ссылались друг на друга, но как оказалось - это плохой дизайн и лучше иметь локальные числовые идентификаторы, а не глобальные умные следящие указатели.
Накидайте какой-нибудь граф, который вы хотели сериализовать.
Мне кажется задача бред и такого графа нет(за исключением задачи описания системы типов используемой в сохранении), рулят почти везде локальные числовые идентификаторы, а не моя система.

domix32
15.12.2025 08:49Проблема было две: "сохранить всё состояние программы" и "менять его в реальном времени не перезапуская программу", как мне уменьшить такой скоуп? Сохранить половину состояния программы? :)
Вы всегда можете сделать маленькую программу. Хэлоу ворлд, REPL c финбонначи и мемоизацией, систему управления лифтами, пинг-понг с роботом, симулятор веб или почтового сервера. Заодно будет фикстурой для юнит-тестов.
"менять его в реальном времени не перезапуская программу"
для этого придумали скриптование. делаете какой-нибудь ctrl+r и триггерите переинициализацию скрипта. hotreload никогда не требовал сериализации графа.
текстуры/шрифты/спрайты_в_атлассе
заводите класс реестра ресурсов и просто ссылаетесь на ресурс по некоторому составному id - resource id + offset id. В качестве resource id можно взять например djb2 хэш от пути к ресурсу. Offset id зависит от того как вы получаете конкретный элемент в ресурсе. Пару широких интов определённо проще сериализовать чем возиться со ссылочными типами, которым ещё некоторую персистентность надо навести.
Ссылочные типы очень пригодились для описания типов используемых внутри сохраняемого файла.
Так а используется-то оно как? Выглядит как несколько странный дебаггинг, судя по вашему описанию.
Мне кажется задача бред и такого графа нет
Ну, вы же решали какую-то свою боль, изобретая вот это вот всё. Пусть криво-косо и вероятно без знания предметной области, но вполне что-то решили в итоге. Это нормально. Сделайте минимально воспроизводимую боль. Пусть без окон и imgui крутилок, но понятную другим.
Хотя хаб "ненормальное программирование" я бы всё же добавил.

Adler3D Автор
15.12.2025 08:49для этого придумали скриптование. делаете какой-нибудь ctrl+r и триггерите переинициализацию скрипта. hotreload никогда не требовал сериализации графа.
Ну чистая правда, даже придраться не к чему. Хотя есть: скрипты - это иногда медленно; структуры данных внутри скриптов также может меняться и ломать миграцию молча и противно. Можно попробовать парировать отсылкой к JSON-based системам миграции, но это опять же медленно и не лишено кучи недостатков.
заводите класс реестра ресурсов и просто ссылаетесь на ресурс по некоторому составному id - resource id + offset id. В качестве resource id можно взять например djb2 хэш от пути к ресурсу. Offset id зависит от того как вы получаете конкретный элемент в ресурсе. Пару широких интов определённо проще сериализовать чем возиться со ссылочными типами, которым ещё некоторую персистентность надо навести.
100% правда. я так поступил из-за своего неумения проектировать программы. я даже не могу возразить что у меня есть плюс - типа мои ссылки быстрее работают, а вот нифига, id+hash рулят и педалят походу. Всё что меня спасает - это отсутствие коллизии хэшей вообще.
Так а используется-то оно как? Выглядит как несколько странный дебаггинг, судя по вашему описанию.
Используется при миграции между форматами.
Ну, вы же решали какую-то свою боль, изобретая вот это вот всё.
ну я научился сохранять всё состояние программы со всеми ссылками и метаинфой, получил крутую миграцию. боль была в том, что предыдущая система сериализации не умела сохранять обычные не полиморфные типы данных. А это очень полезно для работы по сети, т.к там требуется компактность и скорость. В скорости я проиграл из-за того что интерпретирую метаинформацию при обходе дерева/графа, а вот компактность получил. Правда для сети мне оказались ссылки не нужны и поэтому я сделал ещё одну версию сериализатора без них. И она победила из-за своей скорости компиляции.
Сделайте минимально воспроизводимую боль. Пусть без окон и imgui крутилок, но понятную другим.
ИИ подсказывает что можно сделать демонстрацию профита от моей системы на примере системы из publisher-subscriber и их миграции в новый формат. Попробую сделать. Спасибо за то что навели на хорошую идею примера!

Jijiki
15.12.2025 08:49тоесть вы хотите раскрутить цикл знать его кадр и текущий стек?
а зачем это нужно именно в текстовой информации это тогда сериализация на уровне языка получается
тогда проще написать виртуальную машину, если есть требование такой отладки, там и стек и данные вы будете контролировать и приемлемый уровень сериализации всего стека если захотите, но ценой исполнения вм(фиббоначи(24 100000 итераций) в 10 раз медленнее например я тестил у себя)
возможно можно как раз jit код сериализировать, но тут вопросов больше будет
тоесть jit код на цикл, метим цикл и он уходит в таблицу функции файла такого-то
ллвм может умеет такое
байт-код это частный случай сериализации, но раскрутка и фреймы стека выделяются во время исполнения например
вот например таблица программы
Скрытый текст
0 // ALLOCATE_STACK 1 4 3 // LOAD 1 5 // PUSH 1 7 // CMPLT 8 // JUMP_IF_FALSE 14 10 // LOAD 1 12 // JUMP 0 14 // PUSH 0 16 // STORE 2 18 // PUSH 1 20 // STORE 3 22 // PUSH 0 24 // STORE 4 26 // LOAD 1 28 // PUSH 1 30 // CMPGT 31 // JUMP_IF_FALSE 52 33 // LOAD 2 35 // LOAD 3 37 // ADD 38 // STORE 4 40 // LOAD 3 42 // STORE 2 44 // LOAD 4 46 // STORE 3 48 // DEC 1 50 // JUMP 26 52 // LOAD 3 54 // RET 55 // ALLOCATE_STACK 0 4 58 // PUSH 0 60 // STORE 1 62 // PUSH 0 64 // STORE 2 66 // PUSH 100000 68 // STORE 3 70 // LOAD 2 72 // LOAD 3 74 // CMPLT 75 // JUMP_IF_FALSE 88 77 // PUSH 24 79 // CALL 0 1 82 // STORE 1 84 // INC 2 86 // JUMP 70 88 // LOAD 1 90 // PRINT 91 // LOAD 2 93 // PRINT 94 // PUSH 0 96 // HALTтоесть возможно лучше создать тогда какой-то скриптовый язык, встроить его в С++ и ему скармливать стек наверно или обьекты
но проблема еще в том, что стек это не данные, получается лисп код это данные-код

Adler3D Автор
15.12.2025 08:49а зачем это нужно именно в текстовой информации
текстовый формат у меня чисто для отладки
тогда проще написать виртуальную машину, если есть требование такой отладки
виртуальная машина не решает проблемы миграции данных между разными(старой и усовершенствованной) системами типов, но зато позволяет менять код что очень круто, правда, как вы верно заметили, ценой замедления программы в 10-50 раз, что для меня не приемлемо.

Jijiki
15.12.2025 08:49если конвенции стандартные со скоростью может помочь трансляция кода из таблицы в машинный код просто, просто получится тогда как шейдерный язык(аля шейдерный но на процессоре, но это много кода требует конечно, тоесть виртуалка+ассемблирование)
и наверно придётся отслеживать очередь запущенных оттранслированных таблиц типо
подход с виртуальной машиной как вы заметили даёт возможность удобств каких-то, но некст уровень или альтернативный гнать из байткода в ассемблер
поидее можно создать живую вм и создать функционал обновления типа наверно не знаю пока
ну форт или лисп получается, ну clojure вроде нормально, совмещает обе концепции

Adler3D Автор
15.12.2025 08:49мы здесь не сериализуем некоторые вещи связанные с выполнением программы, а именно:
не сохраняем стек вызовов
не сохраняем PC / позицию выполнения
не трогаем низкоуровневые детали вроде сетевых или GPU-состояниймы тут сериализуем почти всё состояние данных, включая ссылки и типы, чтобы:
обновлять бинарь
мигрировать структуры
продолжать работу без VMVM/JIT решают другую задачу - управление кодом, а не миграцию данных между версиями типов.

Jijiki
15.12.2025 08:49можно транслировать в бинарь, ладно я вас понял, извините, так состояние можно кидать на байткод и его патчить(в вашем случае есть 2 опции, посмотреть для отладки наверно или миграции или обновлении/транслировать в бинарь получается), а транслятором собираем бинарник, при этом виртуалка должна быть с реализованным языком(это я забыл указать, тоесть там получается вы не просто вм встраиваете в С++, а по-сути DSL как я понимаю с ништяками), получается некий велосипед если самому с анси си стартовать, но по итогу имеем комплекс отладка/патчинг/прогонка, там элегантно получается, ладно извините(про гпу я писал как пример, но кстати пример с гпу собирает тоже бинарь)

Adler3D Автор
15.12.2025 08:49попросил ИИ перевести на понятный мне язык, вот его цитата:
«Я понял, что вы не сериализуете стек и выполнение.
Тогда, если уж есть описание состояния, его можно:представить как байткод
патчить
при желании транслировать в нативный код
Но для этого придётся сделать язык + VM + транслятор,
то есть фактически DSL, а не просто сериализацию данных.
Это сложно, похоже на изобретение велосипеда,
но в итоге можно получить мощный комплекс отладки и патчинга.»интерпретация от ИИ:
Он согласился с тобой, но:
не может выйти из парадигмы «всё = код»
поэтому постоянно тянет разговор к VM / DSL / байткоду
Ты же работаешь в другой оси:
данные эволюционируют
код остаётся нативным
Он этого ментально не различает.
вот что я думаю читая это:
что бы я сделал если бы нашёл кого-то кто про финансирует open-source разработку? - свой язык программирования который сразу без танцев с макросами и шаблонами понимает мою систему RTTI/STTI/указатели и при миграции между форматами генерирует настоящий нативный код для сохранения и загрузки состояния в этом новом формате, плюс ещё если к этому добавить safe_eval+unsafe_eval, то вообще было бы шикарно.

Jijiki
15.12.2025 08:49если элегантно воспользоваться деревом в сцене и деревом в ui, я не знаю какой проект у вас допустим 3д, есть 3д-обьект, 2д-обьект, есть таблица таблица это интерпретация функции некоего блюпринта, чтобы создать узел визуальный будь 2д-обьект прикрепленный к 3д-обьекту, надо создать функцию-таблицу, интерпретировать её, и эту связь положить на деревья обьектов(например персонаж с текстурой над ним - иконкой), значит узлы можно кодировать отдельно в текстовом формате, просто при запуске, мы билдим эти деревья учитывая таблицы.
как это по переносимости, так вот я вам описал как это можно сделать, можно языком в виде dsl, можно просто парсить, просто эта логика будет как отдельный елемент вашей реализации, а не как макрос, и обьекты в нодах сцены и нодах юи уже стандартизированы будут если это, например BVH ноды - а именно 2 разных корня под юи и текущую сцену
тоесть мы создаём 2 функции на 1 блюпринт в котором у нас связаны какието 2 обьекта, а именно 3добьект с 2д обьектом, из таблицы(принципиально это слово означает создание держателя ресурсов) ресурсов, которую при инициализации можно получить
в конкретном случае мы смотря от реализации либо остаёмся в изоляционных нодах, тогда парсер билдит связи по координатам, или генерируем взависимости от подхода новый уникальный узел, мне просто изоляция понравилась

Adler3D Автор
15.12.2025 08:49ИИ подсказывает что вы хотите узнать зачем вам вообще мигрировать типы, если можно описывать всё через таблицы и каждый раз собирать сцену заново? То есть вы предлагаете перейти на декларативную сборку сцены тому кто занимается задачей "миграцией долгоживущего состояния сцены + эволюции типов"? Если так то спасибо за идею, но мне мой подход больше нравится. почему/зачем я так делаю/сделал? - потому что раньше мне казалось что это путь к идеальной архитектуре программ, а перестирать сцену каждый раз - это слишком простой/слабый/тупиковый путь. сейчас я менее категоричен и вижу плюсы обоих подходов и думаю что будущее за гибридным решением.

Jijiki
15.12.2025 08:49смотрите, есть такая ситуация, дополню то что я выше написал, 3 точки
инициализация ресурсов
инициализация 2х деревьев т.к. они приведены к абстрагированному обьекту (дерево 3д, дерево 2д)
состояние отрисовки(отдельно проход сцена с пасами, отдельно юи)
обновление
теперь представим такую ситуацию, код С++ как решето запускает только 3 точки, всё остальное работает как емакс,
вы имеете порядок связей подобно стеку вызовов
дерево поидее в конкретном узле найдя в стеке вызовов произведенную связь изменить в двух деревьях адресно 2 обьекта, удалив-занулив эти 2 узла, и либо оставить их так, либо на место нулей поставить другие обьекты, другую модельку или другую текстуру над моделькой
соотв всю ситуацию с ресурсами можно куда-то перенести - назовём это что-то окружением только для работы с этим, язык который я предложил в первом коментарии работает по виртуальному принципу стеквой логики, тоесть у нас обьекты становятся и таблица ресурсов обьектов обьектами языка или ими можно управлять в над С++ ье и получается С++ ничем не управляет, а только делает инит, инит деревьев,рисует и апдейт. Тоесть даёт в виртуалку сначала ресурсы, потом эти деревья, а языком мы связываем что нам нужно

izibrizi2
15.12.2025 08:49На вас подействовало проклятье $mol'а :)
https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
Kotofay
Поищите библиотеку "POST--" там было что то подобное.
https://github.com/knizhnik/POST--
Adler3D Автор
У меня не простое хранилище, а полноценная система, так что вы направляете меня/читателей скорее всего не туда.
ну и там походу даже своих умных указателей нет.
Adler3D Автор
Там даже в README прямым текстом:
POST++ — это persistent object store на mmap, а не сериализация с миграциями.
Он предполагает стабильный layout, адреса и не поддерживает автоматическое изменение типов.
В моей задаче ключевыми были самоописание формата и обновление бинарника без потери данных, поэтому подход принципиально другой.