#compiler
Теория девиртуализации😶
Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта.
Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный😐
Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
1️⃣ Метод класса помечен как
Смысл в том, что даже в случае работы с объектами
2️⃣ Класс является финальным. Смысл в том, что указатель на этот класс не будет указывать на какого-то наследника, который что-то мог бы переопределить, потому что у такого класса просто не может быть наследников.
😁 Этот прикол я обнаружил в исходнике Clang.
3️⃣ Мы работаем с объектом класса, а не с указателем на класс. В этом случае точный класс объекта известен на этапе компиляции.
4️⃣ Объект является prvalue. В C++ есть укуренная классификация объектов, где prvalue (pure value) это грубо говоря выражение которое создает новый объект. Смысл в том, что в этом случае тоже известен точный класс объекта на этапе компиляции.
В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редкихпокемона кейса, оба противоречат ООП:
(1) работа с
(2) Класс
Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.
Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.
Например, в Apple macOS👩💻 есть Kext (Kernel Extension) - расширения ядра, запускающие то или иное несовместимое с оригинальным маком оборудование. Особенность этих Kext в том, что они могут в рантайме менять vtable, поэтому нельзя делать оптимизации, которые обходят обращение к vtable. В Clang есть флаг -fapple-kext для такой настройки.
А в "обычных" окружениях vtable лежат в секциях наподобии
Теория девиртуализации
Виртуальные функции обычно работают через vtable. Если метод виртуальный, то вместо вызова точно известного метода, в рантайме вычисляется адрес метода, который зависит от динамического типа объекта.
Однако в некоторых случаях компилятор может "доказать", что он точно "знает" метод, который надо вызвать, несмотря на то, что метод виртуальный
Два простых примера: класс без final (нет оптимизации), класс с final (есть оптимизация - девиртуализация). Девиртуализованный вариант меньше дергает память.
void CallDo(TDerived& obj) {
obj.Do(); // будет ли девиртуализация?
}
Компилятор считает, что можно девиртуализовать вызов в таких случаях:final.Смысл в том, что даже в случае работы с объектами
TDerived*/TDerived& (которые могут указывать на наследника TDerived) нужный метод будет одним и тем же, как его определил класс TDerived.struct TDerived : IBase {
void Do() final override; // слово `final` тут
};
struct TDerived final : IBase { // слово final тут
void Do() override;
};
Однако есть еще одно условие, когда класс считается финальным - если у него финальный деструктор struct TDerived : IBase {
~TDerived() final = default; // слово final тут
void Do() override;
};
TDerived derived;
derived.Do(); // это же TDerived, инфа 100%
TDerived MakeDerived(); // просто функция
// ...
MakeDerived().Do(); // здесь будет девиртуализация
TDerived{}.Do(); // здесь тоже девиртуализация
На этом всё! Эта оптимизация логичная и скучная, потому что никаких чудес ожидать не приходится. Смысл в том, чтобы доказать что TDerived/TDerived&/TDerived* указывает именно на объект TDerived, а не на какой-то его потомок.В реальном мире девиртуализация отрабатывает нечасто, так как надо, чтобы совпали два редких
(1) работа с
TDerived* вместо IBase*;(2) Класс
TDerived или нужный метод финальный (не помню когда в последний раз писал final).Если вы хотите почитать про девиртуализацию "с нуля" с картинками, то есть крутой лонгрид.
Девиртуализация в C++ не гарантирована. В большинстве своем правила выше работают, но не всегда. В каких-то случаях оптимизировать вызовы виртуальных методов запрещено.
Например, в Apple macOS
А в "обычных" окружениях vtable лежат в секциях наподобии
.rodata. Эта секция защищена на уровне операционной системы - программа обычно сразу падает при попытке сделать туда какую-нибудь запись в рантайме.Please open Telegram to view this post
VIEW IN TELEGRAM
👍21🔥5🖕1
#compiler
Почему constexpr в компиляторах C++ развивается не в ту сторону🤨
1.5 года назад была написана статья "Дизайн и эволюция constexpr в C++". Там описывается эволюция возможностей constexpr (его вычисление происходит прямо в компиляторе). Потом эту статью перевели на английский PVS-Studio и даже упомянули в твиттере Standard C++😀
Вычисление constexpr-выражений исторически связано с алгоритмом под названием constant folding (wikipedia). Алгоритмы этого рода работают на уровне AST (Abstract Syntax Tree), и изначально были нужны для вычисления простейших выражений с целыми числами. В коде выражение
Вот так обычно вычисляются constexpr-выражения компилятором:
1️⃣ Компилятор сейчас строит AST из исходника на C++.
2️⃣ Встречается выражение, которое нужно вычислить "здесь и сейчас", наподобии такого:
3️⃣ Компилятор вычисляет constexpr-выражение на основе текущего AST, и сразу использует результаты для продолжения построения AST.
4️⃣ Полностью готовый AST переводится в "модуль" LLVM IR (это байткод - промежуточное представление перед переводом в ассемблер).
Проблема в том, что constexpr становится вездесущим и поддерживать его все сложнее и сложнее. Например, до сих пор нет нормального
Хватит это терпеть! Подумал кое-кто, и сейчас делает... всё тот же интерпретатор C++ внутри Clang на основе принципиально нового байткода: ConstantInterpreter (статья от авторов).
Эта штука намного быстрее, чем алгоритмы на AST, но не решает главной проблемы - все равное создается ненужный интерпретатор C++!
Я предложил сделать аутсорс constexpr-вычислений на реальное вычисление на процессоре: тема на форуме clang.
Когда мы собираем "модуль" LLVM IR, мы можем запросить выполнение какого-то кода в формате LLVM IR с использованием данных этого модуля (на процессоре текущего компьютера). Лучше всего это видно на примере этих штук:
1️⃣ Туториал по созданию своего языка программирования с LLVM - раздел 3.5.
2️⃣ Программа lli для выполнения LLVM IR.
3️⃣ clang-repl - вообще балдёжная программа, натуральный интерпретатор C++.
Вот так мог бы вычислять constexpr-выражения компилятор:
1️⃣ Компилятор строит AST, одновременно держится выделенный "модуль" LLVM IR.
2️⃣ Каждое constexpr-выражение вычисляется с использованием этого выделенного "модуля" на процессоре компьютера. В этот модуль пихались бы все данные, нужные этому выражению. В общем, выражение выполняется как в 3️⃣ Полностью готовый AST переводится в новый "модуль" LLVM IR.
И можно выкинуть огромные куски кода для вычисления выражений там на AST.
Конечно, ничего из этого не вышло. Мейнтейнеры компилятора немедленно забросали меня говном за такую харамную идею. Я просто не стал разбирать по частям всё ими написанное, чтобы не вступать в бесполезный спор, настолько безапеляционные были ответы.
Основная мантра с их стороны была про проблемы с
Таким образом, теперь можно понять, почему constexpr в C++ медленно развивается, и возможно никогда не станет полноценным (потому что интерпретировать C++ на AST - плохая идея).
Почему constexpr в компиляторах C++ развивается не в ту сторону
1.5 года назад была написана статья "Дизайн и эволюция constexpr в C++". Там описывается эволюция возможностей constexpr (его вычисление происходит прямо в компиляторе). Потом эту статью перевели на английский PVS-Studio и даже упомянули в твиттере Standard C++
Вычисление constexpr-выражений исторически связано с алгоритмом под названием constant folding (wikipedia). Алгоритмы этого рода работают на уровне AST (Abstract Syntax Tree), и изначально были нужны для вычисления простейших выражений с целыми числами. В коде выражение
4 + 5 * 6 выглядит в AST примерно так:+Поэтому легко написать рекурсивный алгоритм по вычислению этого добра, и потом итеративно добавлять возможности.
├── 4
└── *
├── 5
└── 6
Вот так обычно вычисляются constexpr-выражения компилятором:
template<int N> class Kek { /* ... */ };
// ...
using Kek34 = Kek<4+5*6>; // точное значение аргумента мне запили!
Проблема в том, что constexpr становится вездесущим и поддерживать его все сложнее и сложнее. Например, до сих пор нет нормального
constexpr std::vector<T>, хотя его должны были сделать еще 4 года назад. Также исправлять баги constexpr реально очень трудно - требуется куча времени, чтобы вникнуть в код. Это своеобразный интерпретатор С++ на AST.Хватит это терпеть! Подумал кое-кто, и сейчас делает... всё тот же интерпретатор C++ внутри Clang на основе принципиально нового байткода: ConstantInterpreter (статья от авторов).
Эта штука намного быстрее, чем алгоритмы на AST, но не решает главной проблемы - все равное создается ненужный интерпретатор C++!
Я предложил сделать аутсорс constexpr-вычислений на реальное вычисление на процессоре: тема на форуме clang.
Когда мы собираем "модуль" LLVM IR, мы можем запросить выполнение какого-то кода в формате LLVM IR с использованием данных этого модуля (на процессоре текущего компьютера). Лучше всего это видно на примере этих штук:
Вот так мог бы вычислять constexpr-выражения компилятор:
clang-repl
И можно выкинуть огромные куски кода для вычисления выражений там на AST.
Конечно, ничего из этого не вышло. Мейнтейнеры компилятора немедленно забросали меня говном за такую харамную идею. Я просто не стал разбирать по частям всё ими написанное, чтобы не вступать в бесполезный спор, настолько безапеляционные были ответы.
Основная мантра с их стороны была про проблемы с
cross-compilation (когда программу собирают под другую платформу). Видимо, есть на свете такие платформы, что там 4+5*6 равняется не не 34, а чему-то еще.Таким образом, теперь можно понять, почему constexpr в C++ медленно развивается, и возможно никогда не станет полноценным (потому что интерпретировать C++ на AST - плохая идея).
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔7👍5👎3🔥1😢1🖕1
#books
Обзор книги "Linux Kernel Development" (2010 г.) 📚👩💻
(можно скачать PDF тут, лучше читать оригинал на английском, а не перевод Гоблина)
Автор этой книги писал код для ядра Linux на протяжении 15 лет (на момент написания книги) в качестве основной работы и сделал там много хорошего.
В этой книге на 400+ страниц содержится информация, нужная для начала разработки кода в ядре Линукса, по состоянию на релиз 2.6.34. Объем информации большой - ведь во многих open source проектах достаточно вникать в код всего несколько дней или часов, чтобы туда что-то написать.
В книге описание "как отправить патч" есть только в одной главе в конце, а в остальном информация выглядит так:
1️⃣ Старые песни о главном: как работает
2️⃣ Копипаст сишной структуры из исходного кода и описание каждого его поля.
3️⃣ Описание разнообразных настроек, которые могут повлиять на каждую сферу работы ядра.
4️⃣ Иногда описание используемой структуры данных (например красно-черных деревьев)
Ядро написано на ISO C99 и активно используеткостыли GNU C Extensions. Я отметил самые крутые на мой взгляд особенности разработки ядра, о которых не задумываются в "обычных" проектах:
⭕️ Ядро не использует стандартную библиотеку C. Нужные функции просто скопипасчены. Например, вместо
⭕️ Почти нет операций с float, потому что для этого требуется ручная работа с float-регистрами процессора.
⭕️ Стек ядра (kernel stack) жестко ограничен - от 4KB до 16KB.
⭕️ Самый часто используемый инструмент синхронизации - спинлок, с "активным" ожиданием разблокировки в while-цикле. Он обусловлен тем, что в ядре всё происходит сравнительно быстро и дожидаться разблокировки в цикле выходит "дешевле", чем делать всю байду с укладыванием процесса в "сон" и его "пробуждение".
⭕️ Для аллокации страничной памяти есть
⭕️ Часто используются "аллокаторы маленьких объектов". Например,
⭕️ Ядро нельзя нормально дебажить, все пишут логи, чтобы понять что происходит. 😈
После этой книги я понял, насколько Linux огромен. Можно сказать, что эта книга "обо всем и ни о чем". Если можно сказать, что компилятор для C++ это "четкая цель", то ядро Linux это "нечеткая цель" - его можно собрать в миллиарде разных конфигураций.
Например: В описании обработчиков прерываний (interrupt handlers - грубо говоря обработка событий типа "ввод с клавиатуры") мы узнаем, что обработчик должен быть мега-быстрым, потому что пока обрабатывается одно прерывание, все остальные прерывания идут лесом.
Поэтому обработка прерывания разделена на две части -
Так вот, для обработки части😁
И так во всем - везде будет огромное количество опций.
По состоянию на 2010 год официально поддерживалось 60 файловых систем.
Есть целые классы алгоритмов для планировщика процессов.
Несколько методов управления I/O Scheduling (ввод-вывод с жестким диском).
Есть море драйверов, которые можно вкомпилировать в образ ядра по желанию.
Полностью "выучить" Linux невозможно, по каждой части можно писать отдельную книгу📝
Обзор книги "Linux Kernel Development" (2010 г.) 📚
(можно скачать PDF тут, лучше читать оригинал на английском, а не перевод Гоблина)
Автор этой книги писал код для ядра Linux на протяжении 15 лет (на момент написания книги) в качестве основной работы и сделал там много хорошего.
В этой книге на 400+ страниц содержится информация, нужная для начала разработки кода в ядре Линукса, по состоянию на релиз 2.6.34. Объем информации большой - ведь во многих open source проектах достаточно вникать в код всего несколько дней или часов, чтобы туда что-то написать.
В книге описание "как отправить патч" есть только в одной главе в конце, а в остальном информация выглядит так:
fork(), виртуальная страничная память, прерывания, планировщик задач, syscalls, драйвера, и т.д. и т.п.Ядро написано на ISO C99 и активно использует
printf используется функция printk. Еще скопипасчены строковые алгоритмы.vmalloc - аллоцирует виртуально непрерывную память, и kmalloc - аллоцирует физически непрерывную память. "Обычные" программы практически всегда используют vmalloc, но ядро практически всегда использует kmalloc для быстроты, чтобы не возиться со структурами виртуальной памяти. Тред на stackoverflowslab allocator очень напомнил мне блоковый аллокатор из Box2D (хотя я не большой специалист в аллокаторах).gdb не работает нормально, он не может никак модифицировать данные ядра, ставить breakpoint-ы и выполнять код step by step. В книге приводится какая-то укуренная схема с костылем kgdb, где используется два компьютера, и один компьютер дебажит ядро второго через шнур. После этой книги я понял, насколько Linux огромен. Можно сказать, что эта книга "обо всем и ни о чем". Если можно сказать, что компилятор для C++ это "четкая цель", то ядро Linux это "нечеткая цель" - его можно собрать в миллиарде разных конфигураций.
Например: В описании обработчиков прерываний (interrupt handlers - грубо говоря обработка событий типа "ввод с клавиатуры") мы узнаем, что обработчик должен быть мега-быстрым, потому что пока обрабатывается одно прерывание, все остальные прерывания идут лесом.
Поэтому обработка прерывания разделена на две части -
"top half" в interrupt handler (очень быстро сделать вещи, скажем поставить флаг где-то в ядре), и "bottom half" когда-то потом (копировать данные, менять структуры в ядре, блокировать поток и тд и тп).Так вот, для обработки части
"bottom half" по состоянию на 2010 год было 5 механизмов - 2 устаревших и 3 активных И так во всем - везде будет огромное количество опций.
По состоянию на 2010 год официально поддерживалось 60 файловых систем.
Есть целые классы алгоритмов для планировщика процессов.
Несколько методов управления I/O Scheduling (ввод-вывод с жестким диском).
Есть море драйверов, которые можно вкомпилировать в образ ядра по желанию.
Полностью "выучить" Linux невозможно, по каждой части можно писать отдельную книгу
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥16👍4🖕1
#story
Как я решал задачи по CTF😀
CTF (Capture The Flag) это соревнования, где участники решают веселые задачки "типа" по кибербезопасности.Забегая вперед, я не понял при чем тут кибербезопасность.
Пару лет назад я порешал такие задачи, чтобы понять что это такое. Мне понравился сайт ctflearn.com (мой профиль). За несколько вечеров можно порешать несколько десятков задач.
Задачи разделяются на несколько категорий.
Дается какая-то сущность (адрес сайта, архив, бинарник, изображение) и из этой сущности надо вытащить "флаг" - строку наподобии
На мой взгляд, умение решать такие задачи никуда не конвертируется. Это очень сильно "вещь в себе". Рекламируется, что подобные задачи сделаны типа для "хакеров", но это жесткий развод, потому что по-моему хакеры не этим занимаются.
Кроме того, многие задачи никогда не догадаешься как решать, если не решал что-то подобное раньше (с подсказками), поэтому прямо на смекалочку задач не так много.
Какие типичные задачи попадаются и как их решать?
💻 Дается изображение.
Некоторые форматы изображений поддерживают "комментарии" (и прочие метаданные), находим флаг там. "Комментарий" можно увидеть в GIMP или других редакторах (открыв там картинку), но проще всего запустить команду в терминале.
💻 Дается бинарник.
При запуске
💻 Дается бинарник.
По дизассемблеру видим, что требуется "типа" ввести какую-то правильную строку, чтобы показать флаг.
Вообще если где-то в коде есть
Решением является заменить руками несколько байт в бинарнике на инструкцию
💻 Дается бинарник.
Теперь флаг не выводится сам, а просто есть намертво обфусцированные функции. Предлагается вручную восстановить "на бумаге" алгоритм в этих функциях. Такие задачи можно решать до трёх часов (у меня было так), сидя в
Видел, что иногда бывает дизассемблер Python или Java, по идее это должно быть в 100500 раз проще C/C++, но мне такие задачи не встречались.
💻 Дается база данных.
Нужно написать запрос, который покажет ключ. Обычно делается в виде SQL-инъекции, типа вводишь
Однако бывают ублюдские задачи, например в одной из них надо было по-моему знать специфические команды MySQL (которых ни в какой другой СУБД нет), которые покажут все доступные таблицы, и вывести данные из "тайной" таблицы.
💻 Дается изображение.
Теперь флаг реально зашифрован в изображении, а не в его метаданных. Это называется "стеганография".
Часто оказывается, что флаг можно увидеть, скажем посмотрев только зеленую маску (из пикселей
Если это картинка типа "помехи телевизора", можно отзеркалить картинку и наложить на исходную, тогда наложение покажет ключ.
Есть целые тулзы для классов задач(!) Для стеганографии это zsteg.
💻 Дается изображение или другой файл.
Часто нужный файл/файлы "спрятан" сразу после первого. То есть грубо говоря взяли файл картинки и сразу после последнего байта картинки приписали файл архива.
Прикол в том, что просмотрщик картинки игнорирует "лишние" байты, и обнаружить спрятанный архив можно только из терминала.
Это детектится по сигнатурам - например архив начинается с человекочитаемых байтов
💻 Дается звуковый файл.
Обычно его даже не прослушивают😁 А сразу смотрят на спектрограмму файла. Там может находиться флаг или какой-нибудь QR-код.
(продолжение в комментарии к посту, потому что у Телеграма есть ограничение по размеру😤 )
Как я решал задачи по CTF
CTF (Capture The Flag) это соревнования, где участники решают веселые задачки "типа" по кибербезопасности.
Задачи разделяются на несколько категорий.
Дается какая-то сущность (адрес сайта, архив, бинарник, изображение) и из этой сущности надо вытащить "флаг" - строку наподобии
flag{w0w_y0u_ar3_c00l_h@cker}, которая может находиться в неожиданных местах.На мой взгляд, умение решать такие задачи никуда не конвертируется. Это очень сильно "вещь в себе". Рекламируется, что подобные задачи сделаны типа для "хакеров", но это жесткий развод, потому что по-моему хакеры не этим занимаются.
Кроме того, многие задачи никогда не догадаешься как решать, если не решал что-то подобное раньше (с подсказками), поэтому прямо на смекалочку задач не так много.
Какие типичные задачи попадаются и как их решать?
Некоторые форматы изображений поддерживают "комментарии" (и прочие метаданные), находим флаг там. "Комментарий" можно увидеть в GIMP или других редакторах (открыв там картинку), но проще всего запустить команду в терминале.
При запуске
./hack_me выводит хреновню. Однако где-то в нем в открытом виде (не обфусцированном) спрятан флаг. Запускаем strings hack_me (оно ищет человекочитаемые строки) и видим флаг.По дизассемблеру видим, что требуется "типа" ввести какую-то правильную строку, чтобы показать флаг.
Вообще если где-то в коде есть
const char* f = "flag{...}"; то этот flag{...} попадает в бинарь в виде человекочитаемой строки, однако авторы просто обфусцируют этот флаг, чтобы он вычислялся по переусложненной схеме.Решением является заменить руками несколько байт в бинарнике на инструкцию
jmp до вывода флага (чтобы он вывелся без условий).Теперь флаг не выводится сам, а просто есть намертво обфусцированные функции. Предлагается вручную восстановить "на бумаге" алгоритм в этих функциях. Такие задачи можно решать до трёх часов (у меня было так), сидя в
gdb и дизассемблере.Видел, что иногда бывает дизассемблер Python или Java, по идее это должно быть в 100500 раз проще C/C++, но мне такие задачи не встречались.
Нужно написать запрос, который покажет ключ. Обычно делается в виде SQL-инъекции, типа вводишь
1" OR «1» = «1» и получаешь все записи. Напоминаю, что это никак не относится к хакерству, потому что даже самые тупые веб-фреймворки умеют обезопасиваться от этих школьных приемов.Однако бывают ублюдские задачи, например в одной из них надо было по-моему знать специфические команды MySQL (которых ни в какой другой СУБД нет), которые покажут все доступные таблицы, и вывести данные из "тайной" таблицы.
Теперь флаг реально зашифрован в изображении, а не в его метаданных. Это называется "стеганография".
Часто оказывается, что флаг можно увидеть, скажем посмотрев только зеленую маску (из пикселей
(r,g,b) сделать (0,g,0)).Если это картинка типа "помехи телевизора", можно отзеркалить картинку и наложить на исходную, тогда наложение покажет ключ.
Есть целые тулзы для классов задач(!) Для стеганографии это zsteg.
Часто нужный файл/файлы "спрятан" сразу после первого. То есть грубо говоря взяли файл картинки и сразу после последнего байта картинки приписали файл архива.
Прикол в том, что просмотрщик картинки игнорирует "лишние" байты, и обнаружить спрятанный архив можно только из терминала.
Это детектится по сигнатурам - например архив начинается с человекочитаемых байтов
Rar!. По-моему тоже есть тулза специально для CTF, которая детектит такие файлы.Обычно его даже не прослушивают
(продолжение в комментарии к посту, потому что у Телеграма есть ограничение по размеру
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤1🤔1🖕1
#madskillz
Напиши свой собственный RTTI🖱
RTTI (run-time type information) это некая информация о всех виртуальных классах, которая попадает в бинарник программы.
Благодаря этому работают dynamic_cast<> (приведение типа в run-time) и typeid.
Генерацию RTTI можно выключить флагом компиляции
Пусть у нас есть указатель
Есть три варианта приведений типа
1️⃣ Upcast:
Для этого не требуется никакого RTTI. Такое приведение всегда возможно. Можно было бы обойтись static_cast<> (приведение типа в compile-time).
2️⃣ Downcast:
В этом случае обычно используют
Интересно, что если программист совершенно уверен, что приведение возможно, то можно использовать😁
3️⃣ Sidecast:
Такое возможно, если
Скорее всего sidecast значит, что в программе есть серьезные ошибки дизайна🧐
Некоторые проекты не используют "официальный" RTTI😡
Почему так происходит, на примере очень популярных проектов:
1️⃣ Protobuf: вынужденная поддержка проектов с
В protobuf есть базовый для всех "месседжей" класс
Чтобы попробовать сделать downcast от базового класса до класса "месседжа", можно использовать функцию DynamicCastToGenerated.
Как видно по исходнику, если уж нельзя вызвать
Какие ограничения этого подхода: С
2️⃣ LLVM: своя реализация RTTI для быстродействия.
Clang и LLVM имеют большую иерархию типов и делают огромную кучу проверок на типы. Большая часть кода в компиляторах (оптимизации, кодогенерация, ...) завязана на поиск специфических паттернов и операциях на них. Для этого необходимо проверять тип объектов в овер9000 местах, поэтому быстродействие
А быстродействие у
В документации есть крутая статья, как сделать свой RTTI "почти как в LLVM" - How to set up LLVM-style RTTI for your class hierarchy. Для этого заводится специальный
Какие ограничения этого подхода: Дополнительный код -
Всего известно три главных аргумента "против RTTI":
🅱️ Занимает память в бинарнике - очень сомнительный аргумент. RTTI не требует столько памяти, чтобы это стало заметно, по крайней мере в 2023 году.
🅱️ Медленно работает - хороший аргумент, если
🅱️ Его использование - ошибка дизайна - неожиданный аргумент, но именно по этой причине RTTI запрещен в Google C++ Style Guide. По ссылке есть описание "почему это плохо". Конечно, из каждого правила есть исключение.
Напиши свой собственный RTTI
RTTI (run-time type information) это некая информация о всех виртуальных классах, которая попадает в бинарник программы.
Благодаря этому работают dynamic_cast<> (приведение типа в run-time) и typeid.
Генерацию RTTI можно выключить флагом компиляции
-fno-rtti.Пусть у нас есть указатель
X* x (или ссылка X& x). Указатель может указывать на объект с типом не X (но это обязательно будет тип-потомок X).Есть три варианта приведений типа
X к типу Y (тип Y тоже не обязательно реальный тип объекта):Y - класс-предок X.Для этого не требуется никакого RTTI. Такое приведение всегда возможно. Можно было бы обойтись static_cast<> (приведение типа в compile-time).
Y - класс-потомок X.В этом случае обычно используют
dynamic_cast<>. Если окажется, что тип объекта не Y (или не какого-то потомка Y), то приведения не случится.Интересно, что если программист совершенно уверен, что приведение возможно, то можно использовать
static_cast<> и не делать run-time проверку. Если окажется, что он был не прав, то получится undefined behaviour X и Y никак не связаны между собой.Такое возможно, если
X* указывает на объект класса Z:class Z : public X, public Y {...};
Такие касты dynamic_cast<> тоже умеет делать.Скорее всего sidecast значит, что в программе есть серьезные ошибки дизайна
Некоторые проекты не используют "официальный" RTTI
Почему так происходит, на примере очень популярных проектов:
-fno-rtti.В protobuf есть базовый для всех "месседжей" класс
google::protobuf::Message.Чтобы попробовать сделать downcast от базового класса до класса "месседжа", можно использовать функцию DynamicCastToGenerated.
Как видно по исходнику, если уж нельзя вызвать
dynamic_cast<>, то используется костыль - суррогат RTTI: сравнение ссылки на "рефлексию" (уникальное описание "месседжа"). Эту ссылку возвращает виртуальный метод.Какие ограничения этого подхода: С
-fno-rtti доступен только downcast строго на указанный класс - потомок класса Message.Clang и LLVM имеют большую иерархию типов и делают огромную кучу проверок на типы. Большая часть кода в компиляторах (оптимизации, кодогенерация, ...) завязана на поиск специфических паттернов и операциях на них. Для этого необходимо проверять тип объектов в овер9000 местах, поэтому быстродействие
dynamic_cast становится узким местом.А быстродействие у
dynamic_cast сравнительно плохое. Он должен делать обход иерархии наследования и вычисляет путь обхода динамическим образом. Это на несколько порядков медленнее, чем просто вызвать виртуальный метод и что-то сравнить.В документации есть крутая статья, как сделать свой RTTI "почти как в LLVM" - How to set up LLVM-style RTTI for your class hierarchy. Для этого заводится специальный
enum, и каждый класс реализует статический метод classof. Вместо обхода иерархии наследования делается один вызов виртуального метода!Какие ограничения этого подхода: Дополнительный код -
enum, статический метод в каждом классе. Нужно следить за тем, чтобы соответствие между enum и классами не разломалось (хотя тут могут помочь кодогенераторы). Эта схема работает, только если иерархия классов известна заранее (стандартный C++ RTTI такого не требует).Всего известно три главных аргумента "против RTTI":
dynamic_cast<> является узким местом в программе. Но это должна быть специфическая программа, как поиск паттернов в структурах с большой иерархией классов... (например, компилятор C++)Please open Telegram to view this post
VIEW IN TELEGRAM
👍6🖕1
#story
Информационное насилие: курс молодого бойца🔫
Я долго думал, стоит ли писать такой пост, но решил это сделать. Ведь обсуждаться будет не человек, а идеи, которые несутся в массы, а также условия, которые позволяют это делать. Этот текст - не какой-то "лох-патруль", а серьезный пост.
Есть такой деятель, как Андрей Викторович Столяров. Дело в том, что многие люди, которым доводится контактировать с ним, испытывают спектр различных эмоций. Неполный список групп людей, постигших мудрость:
1️⃣ Жители linux.org.ru и прочих форумов. Запомнился розыском CMS на C/C++, критикой термина GNU/Linux, и просто комментариями с нестандартными речевыми оборотами в адрес тех, с кем он несогласен.
2️⃣ Студенты ВМК МГУ. Запомнился ведением занятий по Jabber во время ковида (об этом позже), неодобрительными отзывами на дипломные работы, неуважением к студентам с Windows и/или Code::Blocks на ноутбуке. Также благодаря ему первокурсники учат Pascal.
3️⃣ Его ученики. Запомнились пранкерским разоблачением Rust (rustmustdie.com), в ответ на что неравнодушные люди выпустили разоблачение разоблачения Rust (habr). Затем джихад был объявлен Сишке (cmustdie.com), но это уже было никому не интересно.
4️⃣ Зрители АйТиБороды, посмотревшие 4-часовое интервью. К сожалению, интервьюер не развел его на срач ИРЛ, а то бы просмотров было в 10 раз больше.
У Столярова есть idée fixe - теория информационного насилия. Можно считать, что это новая философская школа мысли, но по факту это просто другое агрегатное состояние "философии" Александра Гельевича Дугина - эклектичный микс разных аксиом. Это рационализаторство собственных предпочтений, оформленное в виде "как бы" философии.
Интересно, что, так сказать, magnum opus Столярова можно посмотреть в формате видеороликов - ссылка на YouTube.
Арсенал цитат из этих видеороликов:
"I don't like spam" - исторический обзор возникновения спама.
"Об основателях рунета" - про каких-то ноунеймов, с которым личные счета из-за смерти ЖЖ.
"Спам из мэрии Москвы" - про чудо спамерской инженерной мысли и письмо в суд.
"В чью пользу авторское право?" - жуткий срыв покровов над книжными издательствами.
"Безобидный гипертекст и коварный джаваскрипт" - просто бриллиант, можно разбирать на цитаты.
(ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ)
Информационное насилие: курс молодого бойца
Я долго думал, стоит ли писать такой пост, но решил это сделать. Ведь обсуждаться будет не человек, а идеи, которые несутся в массы, а также условия, которые позволяют это делать. Этот текст - не какой-то "лох-патруль", а серьезный пост.
Есть такой деятель, как Андрей Викторович Столяров. Дело в том, что многие люди, которым доводится контактировать с ним, испытывают спектр различных эмоций. Неполный список групп людей, постигших мудрость:
У Столярова есть idée fixe - теория информационного насилия. Можно считать, что это новая философская школа мысли, но по факту это просто другое агрегатное состояние "философии" Александра Гельевича Дугина - эклектичный микс разных аксиом. Это рационализаторство собственных предпочтений, оформленное в виде "как бы" философии.
Интересно, что, так сказать, magnum opus Столярова можно посмотреть в формате видеороликов - ссылка на YouTube.
Арсенал цитат из этих видеороликов:
"Пилотный выпуск"Некоторые видео лучше посмотреть самому, так как они просто необычные:
- Информационное насилие - такая штука, исследованием которого я занимаюсь с 97 года.
- Браузер вообще не предназначен для просмотра видео, для этого есть другие программы, а браузером нужно смотреть гипертекст.
"О запрещенных книгах"
- Я признаюсь, я даже "Mein Kampf" читал, и знаете, книжка-то так себе, не умел Гитлер книжки писать.
- Какая-то непонятная шушера за меня решает, что мне читать, а что не читать.
"I don't like spam!"
- Был такой сервис - телеконференция NNTP. Нету больше этих конференций, они в районе 2000 года благополучно загнулись, потому что там оказалось невозможно устроить защиту от спама.
- Тут речь идет не только о рекламе. Многие пытались посылать такое всем получателям своей адресной книги: "А котеночек никому не нужен?" - да не нужен, блин, мне котеночек, и никому из них не нужен котеночек!
"История с телеграмом"
- Я вам открою один секрет. Меня нет в телеграме, меня нет в вотсапе, меня нет вконтактике, меня нет в фейсбуке. В этом зловредном ютубе я появился только потому, что Навальный объявил конкурс, иначе бы меня и здесь не было.
- Мне интересно вот что: когда ВКонтакте отжали у Павла Дурова, хоть бы кто-нибудь стёр свой аккаунт?
- Я ничего не имею против Павла Дурова, ну кроме того что он, зараза, уже вторую проприетарную систему поднимает. <...> Выглядит он красиво, презентабельно, хороший парень такой. Айтишник, опять же.
"В чью пользу авторское право?"
- Информация как таковая не может иметь денежной стоимости.
"Про DRM, Всемирную паутину, ..."
- Техническая стандартизация это особый и особо опасный вид международного терроризма. <...> Такие организации, как ISO, это на самом деле террористические группы.
"I don't like spam" - исторический обзор возникновения спама.
"Об основателях рунета" - про каких-то ноунеймов, с которым личные счета из-за смерти ЖЖ.
"Спам из мэрии Москвы" - про чудо спамерской инженерной мысли и письмо в суд.
"В чью пользу авторское право?" - жуткий срыв покровов над книжными издательствами.
"Безобидный гипертекст и коварный джаваскрипт" - просто бриллиант, можно разбирать на цитаты.
(ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ)
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥9👍6🤡4😱1🖕1
#story
Как сделать программу для читов в играх⌨️
«Я буду устанавливать сейчас все игры»
- Сашко «Компьютерный Монстр»
Когда-то давно я познакомился с программой ArtMoney. Эта программа шла на диске к журналу "Лучшие Компьютерные Игры". Она была очень простой, и я быстро смог настроить бесконечную валюту в игре Spore.
В ArtMoney нужно было указывать процесс игры, затем ввести значение игровой валюты. Программа проверяла всю память и запоминала адреса, где было это значение.
Потом надо было вернуться в игру, что-нибудь сделать, чтобы значение валюты изменилось, и указать новое значение в ArtMoney.
Адреса фильтровались (оставлялись только равные новому значению), и процесс повторялся, пока не останется единственный адрес, в котором можно поменять значение на нужное.
Я решил поисследовать, как работают такие программы. Так как я не пишу под Windows, то исследую аналог для Linux, особой разницы не должно быть😁 Это программа scanmem.
В этой программе также можно искать нужный адрес с переменной. Можно искать не только конкретное значение, но еще промежуток значений, а также "значение по адресу больше/меньше/равно/не равно чем было в прошлый раз" и так далее.
Такое бывает нужно, если например в игре есть "полоска здоровья" без числового значения, про которое известно, что оно становится больше/меньше чем в предыдущий раз.
Проверяется, что
Проверка происходит через
У процесса игры надо увидеть его виртуальное адресное пространство. Это память, которую игра использует и где надо будет фильтровать адреса. Увидеть регионы непрерывно занимаемой памяти может любой (в смысле не только суперюзер), прочитав файл
Те, кто не программируют активно под Linux, могут удивиться двум специфичным вещам:
1️⃣ Очень многие данные о системе/процессе можно увидеть, прочитав особые файлы, хотя видно что это НЕ "стандартные" файлы, данные в них генерируются на ходу! Это концепция "everything is a file". 😐
2️⃣ Программы реально завязываются на формат вывода линуксовых тулз/файлов, в этом ничего страшного нет. Например, весь Android завязан на вывод тулзы
В первом способе (как бы вы думали?) снова открывается файл, на этот раз
Во втором способе можно читать память через ptrace:
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Как сделать программу для читов в играх
«Я буду устанавливать сейчас все игры»
- Сашко «Компьютерный Монстр»
Когда-то давно я познакомился с программой ArtMoney. Эта программа шла на диске к журналу "Лучшие Компьютерные Игры". Она была очень простой, и я быстро смог настроить бесконечную валюту в игре Spore.
В ArtMoney нужно было указывать процесс игры, затем ввести значение игровой валюты. Программа проверяла всю память и запоминала адреса, где было это значение.
Потом надо было вернуться в игру, что-нибудь сделать, чтобы значение валюты изменилось, и указать новое значение в ArtMoney.
Адреса фильтровались (оставлялись только равные новому значению), и процесс повторялся, пока не останется единственный адрес, в котором можно поменять значение на нужное.
Я решил поисследовать, как работают такие программы. Так как я не пишу под Windows, то исследую аналог для Linux, особой разницы не должно быть
В этой программе также можно искать нужный адрес с переменной. Можно искать не только конкретное значение, но еще промежуток значений, а также "значение по адресу больше/меньше/равно/не равно чем было в прошлый раз" и так далее.
Такое бывает нужно, если например в игре есть "полоска здоровья" без числового значения, про которое известно, что оно становится больше/меньше чем в предыдущий раз.
Проверяется, что
scanmem был запущен из-под суперюзера (sudo scanmem), без этого нельзя "следить" за другим процессом (если он не является процессом-"ребенком").Проверка происходит через
getuid() == 0 - uid суперюзера равен 0.У процесса игры надо увидеть его виртуальное адресное пространство. Это память, которую игра использует и где надо будет фильтровать адреса. Увидеть регионы непрерывно занимаемой памяти может любой (в смысле не только суперюзер), прочитав файл
/proc/<pid>/maps:cat /proc/<pid>/maps # вывод в терминалВывод примерно такой:
559b8c5d1000-559b8c5f2000 rw-p 00000000 00:00 0 [heap](подробнее об этих файлах можно прочитать тут)
7faa72001000-7faa72023000 rw-p 00000000 00:00 0
7faa72023000-7faa72055000 r--p 00000000 08:30 3023 /usr/lib/locale/C.UTF-8/LC_CTYPE
7faa726e0000-7faa726e1000 rw-p 0002d000 08:30 11854 /usr/lib/x86_64-linux-gnu/ld-2.31.so
7faa726e1000-7faa726e2000 rw-p 00000000 00:00 0
7ffeb2f53000-7ffeb2f74000 rw-p 00000000 00:00 0 [stack]
scanmem просто открывает этот файл (через fopen) и парсит каждую строку. Оставляются только регионы с возможностью записи (флаг w). Обычно этого флага нет у загруженных секций .text (исполняемый код бинарника) и .rodata (константные данные).Те, кто не программируют активно под Linux, могут удивиться двум специфичным вещам:
wpa_supplicant (для поиска WiFi).scanmem ждет, пока юзер введет текущее значение из игры. После ввода значения вызывается ptrace для слежки за другим процессом (target - process id процесса игры), из-за этого процесс игры остановится (кинет сигнал SIGSTOP):ptrace(PTRACE_ATTACH, target, NULL, NULL)Затем можно читать память процесса. Делать это можно двумя способами.
В первом способе (как бы вы думали?) снова открывается файл, на этот раз
/proc/<pid>/mem.Во втором способе можно читать память через ptrace:
ptrace(PTRACE_PEEKDATA, target, cur_address, NULL)
Посмотрим на первый способ. В первый раз в этом файла читается вся память из занимаемых регионов, через pread. Из чужой памяти загружается 1 мегабайт (брать больше смысла нет), запоминаются все адреса с нужным значением, затем читается следующие 1 мегабайт, и так далее, пока не прошерстят всю память.ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🔥8😁1🖕1
#madskillz
Нестандартные представления строк✨
В "стандартном" C++ есть три основных представления для строк. Не будем учитывать "составные" классы (как
=====
1️⃣
=====
2️⃣
Маленькие строки полностью помещаются на стек (это называется small string optimization), но пока проигнорируем это.
=====
3️⃣
Не обязательно верно то, что😁 Ведь
=====
Класс
4️⃣
Запись
=====
В бизнес-логике со строками есть проблема. Иногда в коде надо делать много составных строк. Например, для создания строки - "версии" программы нужно сложить несколько строк-частей:
В кодовой базе LLVM есть решение, которое сложно для понимания, но мы его разберем:
5️⃣
Трудности начинаются на уровне названия класса, как у не-носителя английского😁 Я так и не понял смысл названия.
Вообще, трудности сначала были со словом
Теперь посмотрим на слово
</конец бесполезного абзаца>
Этот класс опасный: он полагается на стремное правило Reference Lifetime Extension, а также на не менее стремное правило, что объекты, созданные для использования в full-expression, не удаляются до конца выполнения этого full-expression (сформулировал как смог).
Функция должна принимать
😁
😟 )
Нестандартные представления строк
В "стандартном" C++ есть три основных представления для строк. Не будем учитывать "составные" классы (как
std::stringstream), у которых нет уникальных концепций.=====
const char* - просто указатель на начало строки где-то в памяти. Обычно если итерироваться по указателю, то когда-то достигнем нулевой байт \0 (нуль-терминатор), который указывает на конец строки. Все строковые алгоритмы Си завязаны на признак \0 как на конец строки.=====
std::string - класс строки, владеющий памятью для нее в куче. Запись std::string s = "abcd"; значит, что где-то в куче занята память под байты abcd\0. Известно, std::string гарантированно нуль-терминирован (начиная с C++11).std::string_view - класс строки, не владеющий памятью. Представляет собой пару const char* s (начало строки) и size_t len (длину строки).Не обязательно верно то, что
*(s + len) == '\0'. std::string_view указывает не на всю строку, а только на какую-то ее часть.=====
Класс
std::string поведением похож на контейнер std::vector<char>. Можно посмотреть на какие-нибудь неклассические контейнеры, чтобы создать новые строковые классы, которых нет в стандартном C++.SmallString - класс строки, владеющий памятью для нее, с поведением как у SmallVector<char>. Реализован в LLVM.Запись
std::string s1;Дает два объекта
SmallString<256> s2;
s1 и s2, у которых одинаковый набор методов, но s2 хранится на стеке, если размер строки не превышает 256 символов (планируется, что так будет в 99.9% случаев). Если размер все-таки превысили, то строку начинают хранить в куче.=====
В бизнес-логике со строками есть проблема. Иногда в коде надо делать много составных строк. Например, для создания строки - "версии" программы нужно сложить несколько строк-частей:
Major + "." + Minor + "." + VersionPatchВ этом случае происходит создание 3 (!) лишних "временных" строк с аллокациями памяти, то есть делается строка
Major + ".", потом строка (Major + ".") + Minor и так далее. Более того, итоговая строка (4-я по счету) тоже по сути лишняя, если мы хотели сразу записать итог в какой-нибудь файл, а не хранить результат сложения.В кодовой базе LLVM есть решение, которое сложно для понимания, но мы его разберем:
Twine - класс "сумма строк". Документация по Twine, но больше информации в исходнике.Трудности начинаются на уровне названия класса, как у не-носителя английского
Вообще, трудности сначала были со словом
string. До того, как я начал программировать, у меня это ассоциировалось со стрингами, которые носил Борат. У этого слова куча значений, пусть в нашем случае это будет шнур.Теперь посмотрим на слово
twine. У него тоже вагон значений, пусть в нашем случае это будет бечёвка, пнятненько?</конец бесполезного абзаца>
Этот класс опасный: он полагается на стремное правило Reference Lifetime Extension, а также на не менее стремное правило, что объекты, созданные для использования в full-expression, не удаляются до конца выполнения этого full-expression (сформулировал как смог).
Функция должна принимать
Twine по константной ссылке:void foo(const Twine& T);А подавать туда Twine нужно не отходя от кассы, чтобы сработало правило RLE:
foo(Twine(Major) + "." + Minor + "." + VersionPatch);Благодаря правилу про full-expression, все составные части строки "живы" на стеке, пока не выполнится вызов
foo.Twine внутри себя выглядит как бинарное дерево. У него два "ребенка":Child LHS;Каждый ребенок это указатель на какой-нибудь строковой объект:
Child RHS;
const char, или std::string, или std::string_view, или другой Twine ("поддерево"). Также для удобства поддерживаются числа union ChildПРОДОЛЖЕНИЕ В ПЕРВОМ КОММЕНТАРИИ (у телеграма ограничение по размеру постов
{
const Twine *twine;
const char *cString;
const std::string *stdString;
/* ... */
int decI;
/* ... */
};
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥6🙏1🖕1
#opensource
Обзор на Boost🆘
Boost это широко известный набор библиотек для C++. Boost оказал большое влияние на развитие C++, но что осталось от его влияния в 2023 году?
Чтобы не копаться в библиотеках самому, можно почитать про разработку в Boost в крутой книге крутого Антона Полухина, которой уже прилично лет, но ее держат в актуальном состоянии.
Надеюсь, не проспойлерю книгу (на сайте все равно есть исходники примеров), но очень, очень много чего в Boost вошло в стандарты C++, и вы половину книги будете читать про то, как работают классы
Эта часть Boost отдала свои жизненные соки Стандарту C++ и перестала быть интересной (кроме как тем, кто пишет на C++ старого стандарта и не может перейти на более новый стандарт).
Далеко не все куски Boost находятся в ажурном состоянии. Сейчас в Boost состоит 169 библиотек, во многом независимых друг от друга. Практически у всех библиотек есть какие-то реальные проблемы из этих:
1️⃣ Не обновлялась с ~2006 года. Да, некоторые библиотеки просто написаны сумрачным гением тысячу лун назад и заброшены.
2️⃣ Стала неактуальной после вхождения в Стандарт C++. Это описал выше.
3️⃣ Повторяет другие библиотеки по функциональности. Бывают приколы как тупо
4️⃣ Есть нишевая библиотека вне Boost с лучшим функционалом. Можно сравнить Boost.JSON и nlohmann/json.
В целом Boost так себе в нишевых библиотеках, количество контрибьюторов в отдельную библиотеку намного ниже, чем в популярный проект.
Иногда кто-то хочет усугубить проблему и добавить библиотеку по типу
Видимо, делаются попытки с уверенностью, что Boost сам по себе типа как бы бренд, и библиотека становится лучше как бы самим фактом наличия в Boost... Что не так.
5️⃣ Лютая дичь и эрзац-компилятор. Это легендарные библиотеки-монстры, которые выглядят очень странно, потому что нестандартными способами обходят ограничения компиляторов, или просто "чем хуже тем лучше". Я бы отнес их использование к ненормальному программированию.
Например, Boost.Hana для метапрограммирования
Есть библиотеки для имитации
6️⃣ Не лучшая техническая реализация. Некоторые фанаты open source верят в миф, что стоит проекту бытоваться открытым, как тут же появляются "тысячи глаз", которые следят за его качеством. На деле никому это нафиг не нужно это вряд ли так, и большой вопрос, где качество кода в среднем лучше.
Из тех библиотек, что я активно исследовал:
Boost.ScopeExit - в другом опенсорсном проекте есть реализация подобной штуки без необходимости писать
Boost.SmallVector - официально стырен из LLVM, а не придуман уникально.Почему бы тогда не использовать библиотеку LLVM?
Boost.DynamicBitset - по состоянию на 2018 год оно использовало захардкоженные таблицы, чтобы искать количество бит в числе или типа того.
Я туда сделал коммиты (github) и ускорил некоторые методы в 2 раза, если система поддерживает интринсики как
Offtop:в 2018 году я исследовал dynamic bitset, потому что программировал тогда шахматы с кастомным размером доски (не 8x8) и std::bitset<64> не подходил. Но в итоге забросил идею, а запрограммировал шахматы только в 2022 году без dynamic bitset ( мой лонгрид на хабре ).
🤔 Таким образом, в 2023 году Boost может быть не лучшим выбором для активного использования!
Обзор на Boost
Boost это широко известный набор библиотек для C++. Boost оказал большое влияние на развитие C++, но что осталось от его влияния в 2023 году?
Чтобы не копаться в библиотеках самому, можно почитать про разработку в Boost в крутой книге крутого Антона Полухина, которой уже прилично лет, но ее держат в актуальном состоянии.
Надеюсь, не проспойлерю книгу (на сайте все равно есть исходники примеров), но очень, очень много чего в Boost вошло в стандарты C++, и вы половину книги будете читать про то, как работают классы
boost::shared_ptr<T> и boost::string_view. Работают они почти так же, как канонические std::XXX, но иногда отличия бывают (в книге рассказано, какие именно).Эта часть Boost отдала свои жизненные соки Стандарту C++ и перестала быть интересной (кроме как тем, кто пишет на C++ старого стандарта и не может перейти на более новый стандарт).
Далеко не все куски Boost находятся в ажурном состоянии. Сейчас в Boost состоит 169 библиотек, во многом независимых друг от друга. Практически у всех библиотек есть какие-то реальные проблемы из этих:
Boost.Variant и Boost.Variant2.В целом Boost так себе в нишевых библиотеках, количество контрибьюторов в отдельную библиотеку намного ниже, чем в популярный проект.
Иногда кто-то хочет усугубить проблему и добавить библиотеку по типу
Boost.Lua (еще одну к овер9000 библиотекам про Lua), но к счастью количество библиотек растет не так быстро.Видимо, делаются попытки с уверенностью, что Boost сам по себе типа как бы бренд, и библиотека становится лучше как бы самим фактом наличия в Boost... Что не так.
Например, Boost.Hana для метапрограммирования
struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(std::string, name),
(int, age)
);
};
Boost.Spirit как LL-парсер, который притом header-only (поэтому собирается по 10 минут).Есть библиотеки для имитации
std::move до C++11 и прочие попытки перепрыгнуть выше крыши.Из тех библиотек, что я активно исследовал:
Boost.ScopeExit - в другом опенсорсном проекте есть реализация подобной штуки без необходимости писать
BOOST_SCOPE_EXIT_END в конце.Boost.SmallVector - официально стырен из LLVM, а не придуман уникально.
Я туда сделал коммиты (github) и ускорил некоторые методы в 2 раза, если система поддерживает интринсики как
__builtin_popcount.Offtop:
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11🔥6🖕1
#story
Сделай свой std::flat_set из C++23 📦
В C++23 были приняты новые контейнеры
В интернете почти нет нормальных объяснений их содержимого. Многие из тех, что есть, скорее запутывают. Там приводятся всякие ненужные размышления на тему локальности памяти, бенчмарки, итераторы, и так далее.
А на деле реализация этих контейнеров простая, ее может сделать каждый. Из готовой реализации можно уже самому понять свойства этого класса.
API flat_set почти не отличается от set. Добавлены новые конструкторы и перегрузки
Можно просто заменить тип на новый и почти наверняка это скомпилируется.
Новые контейнеры являются адаптерами. Это такие контейнеры, которые сами не делают ничего "тяжелого", а только держат у себя другой контейнер и все методы пробрасывают туда. В C++ уже было три таких контейнера -
Пусть у нас есть😝
0️⃣ Итераторы
1️⃣ Конструктор
2️⃣ Вставка
😁
3️⃣ Удаление
Действуют по одинаковому принципу с
4️⃣ Слияние двух flat_set
В некоторых случаях надо делать слияние двух отсортированных векторов. Это стандартный алгоритм, его часто спрашивают на собеседованиях.
Если нужно вставить несколько элементов, то можно вызвать такой метод, который вставит каждый элемент по отдельности за
🙂
Сделай свой std::flat_set из C++23 📦
В C++23 были приняты новые контейнеры
std::flat_set, std::flat_map (и их multi-варианты), аналогично уже существующим std::set и std::map.В интернете почти нет нормальных объяснений их содержимого. Многие из тех, что есть, скорее запутывают. Там приводятся всякие ненужные размышления на тему локальности памяти, бенчмарки, итераторы, и так далее.
А на деле реализация этих контейнеров простая, ее может сделать каждый. Из готовой реализации можно уже самому понять свойства этого класса.
API flat_set почти не отличается от set. Добавлены новые конструкторы и перегрузки
insert. Заменены редкие методы: node_type extract(...)
insert_return_type insert(node_type&& nh);
то есть те, где юзер должен был работать с вершинами красно-черного дерева.Можно просто заменить тип на новый и почти наверняка это скомпилируется.
Новые контейнеры являются адаптерами. Это такие контейнеры, которые сами не делают ничего "тяжелого", а только держат у себя другой контейнер и все методы пробрасывают туда. В C++ уже было три таких контейнера -
stack, queue, priority_queue:template<class T, class Container = std::deque<T>> class stack;
template<class T, class Container = std::deque<T>> class queue;
flat_set (и ему подобные) такой же адаптер:template<class Key, class Compare = less<Key>, class KeyContainer = vector<Key>> class flat_set;То есть множество представляется (по умолчанию) в виде отсортированного вектора (но можно дать и другой класс -
static_vector, small_vector).Пусть у нас есть
std::vector<T> data. Попробуем реализовать для него все основные методы flat_set сами, в упрощенном виде (без кастомных компараторов и прочего) begin/end/..., методы empty()/size()/max_size()
Подобные методы не рассматриваем, они просто перенаправляют вызовы в сам контейнерdecltype(auto) begin() noexcept { return data.begin(); }
flat_set(container_type cont);
Конструкторов там примерно 24 штуки на разные случаи жизни. Можно реализовать случай, когда дается контейнер, который изначально не отсортирован и там могут быть повторяющиеся элементы - с использованием std::unique:data = std::move(cont);Сложность этого метода
std::sort(data.begin(), data.end());
auto last = std::unique(data.begin(), data.end());
data.erase(last, data.end());
O(NlogN)
insert(const value_type& x)
Нужно найти место, куда вставить новый элемент. Если он уже существует, то вставлять не нужно. Это можно делать бинарным поиском через std::lower_bound и вставкой в вектор в этом место.auto lower = std::lower_bound(data.begin(), data.end(), x);Сложность этого метода
if (lower == data.end() || *lower != x) {
data.insert(lower, x);
}
O(N) в общем случае и амортизированный O(logN) в случае вставки в конец. "Амортизированный" - потому что можно попасть на реаллокацию вектора erase(const key_type& x) и прочие методы (find, contains, ...)Действуют по одинаковому принципу с
insert - найти место этого элемента бинарным поиском и что-то с этим сделать.auto lower = std::lower_bound(data.begin(), data.end(), x);Сложность этого метода
if (lower != data.end() && *lower == x) {
data.erase(lower);
}
O(N) в общем случае и O(logN) в случае удаления с конца.В некоторых случаях надо делать слияние двух отсортированных векторов. Это стандартный алгоритм, его часто спрашивают на собеседованиях.
Если нужно вставить несколько элементов, то можно вызвать такой метод, который вставит каждый элемент по отдельности за
O(N), с итоговой сложностью до O(N^2) в среднем:void insert(InputIterator first, InputIterator last);Но если есть знание, что вставляемые элементы
[first, last) уже отсортированы, то можно вызвать другой метод с "мусорным" объектом в начале, который сделает слияние, это будет работать за O(N), что может быть быстрее:void insert(sorted_unique_t, InputIterator first, InputIterator last);Такую задачу можно решить на Leetcode - Merge Sorted Array
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🖕1
#madskillz
Сделай свой эмулятор процессора Motorola 68000🖥
В свободное время программисты могут заниматься странными вещами. Например, я сделал эмулятор процессора Motorola 68000. Сейчас там поддержано 94% инструкций и все нужные абстракции, это заняло ~2500 строк кода.
Мне было интересно, рекомендую это делать всем, кто хочет лучше понять работу процессора🍅
m68k (сокращенно) это процессор, сильно обогнавший свое время. Он использовался на протяжении десятилетий в компьютерах Macintosh, Amiga, Atari, приставке Sega Mega Drive, и прочих устройствах.
1️⃣ Представление в C++
В архитектуре процессора уже есть элементы 32-битовости, но с ограничениями.
Всего есть 16 регистров 32-битных (и 1 регистр 16-битный).
Несмотря на то, что "адресные" регистры (
Процессор поддерживает зачаток виртуализации для многозадачных систем - обращение к регистру
🔍 registers.h - представление регистров
Процессор может что-то читать/писать по адресам
Эмулятор имеет дело с интерфейсом. Запись/чтение могут спровоцировать ошибку по любой причине (например, чтение по нечетному адресу). Я не использую исключения C++ в эмуляторе - объект ошибки возвращается из методов.
🔍 i_device.h - интерфейс записи/чтения памяти
"Текущее состояние" эмулятора, которое можно менять, можно представлять так:
🔍 target.h - представление операнда в инструкциях
Последнее, самый большое представление - у инструкций. У них есть "тип" инструкции и все нужные параметры. Ассемблер очень "ортогональный", поэтому представление в виде набора переменных подходит лучше всего.
🔍 instructions.h - представление инструкций
2️⃣ Как реализовать и протестировать инструкции
Каждая инструкция занимает 2 байта. Иногда могут потребоваться 2/4 байта дополнительных данных после инструкции (обязательно четное число).
Декодирование инструкции можно написать глядя на крутую таблицу от GoldenCrystal (сайт регулярно лежит, в комментариях к посту есть PDF).
Краткое описание инструкции можно читать в этой markdown-документации. Иногда этого недостаточно, тогда можно читать длинное описание в этой книге.
Самое важная часть - тестирование. Небольшая ошибка в каком-нибудь статусном флаге может привести к катастрофе во время эмуляции. Когда программа большая, ее становится легко сломать в неожиданном месте, поэтому нужны тесты на все инструкции.
Мне очень помогли тесты из этого репозитория. На каждую инструкцию есть 8000+ тестов, которые покрывают все возможные случаи. Суммарно тестов чуть больше миллиона.
Они могут находить даже самые мелкие ошибки - нередко бывает ситуация, что не проходятся ~20 тестов из 8000.
Например, инструкция
Продолжение в комментариях (эмуляция программ)
Сделай свой эмулятор процессора Motorola 68000
В свободное время программисты могут заниматься странными вещами. Например, я сделал эмулятор процессора Motorola 68000. Сейчас там поддержано 94% инструкций и все нужные абстракции, это заняло ~2500 строк кода.
Мне было интересно, рекомендую это делать всем, кто хочет лучше понять работу процессора
m68k (сокращенно) это процессор, сильно обогнавший свое время. Он использовался на протяжении десятилетий в компьютерах Macintosh, Amiga, Atari, приставке Sega Mega Drive, и прочих устройствах.
В архитектуре процессора уже есть элементы 32-битовости, но с ограничениями.
Всего есть 16 регистров 32-битных (и 1 регистр 16-битный).
Несмотря на то, что "адресные" регистры (
A0-A7) 32-битные, по факту для адреса берутся младшие 24 бита. То есть адресуется пространство в 16 мегабайт памяти.Процессор поддерживает зачаток виртуализации для многозадачных систем - обращение к регистру
A7 по факту будет обращением либо к USP, либо к SSP, в зависимости от флага в статусном регистре.Процессор может что-то читать/писать по адресам
0x000000 - 0xFFFFFF (24 бита), не обязательно это будет физическая память. Иногда запись в определенные адреса будет влиять на периферийные устройства. Поведение определяется "шиной".Эмулятор имеет дело с интерфейсом. Запись/чтение могут спровоцировать ошибку по любой причине (например, чтение по нечетному адресу). Я не использую исключения C++ в эмуляторе - объект ошибки возвращается из методов.
"Текущее состояние" эмулятора, которое можно менять, можно представлять так:
struct TContext {
NRegisters::TRegisters& Registers;
NMemory::IDevice& Memory;
};
Операнды в инструкциях ("цели") могут указывать на адрес в памяти/регистр большим количеством способов. Для этих способов можно выделить примерно такой интерфейс с общим методом чтения/записи данных:Последнее, самый большое представление - у инструкций. У них есть "тип" инструкции и все нужные параметры. Ассемблер очень "ортогональный", поэтому представление в виде набора переменных подходит лучше всего.
Каждая инструкция занимает 2 байта. Иногда могут потребоваться 2/4 байта дополнительных данных после инструкции (обязательно четное число).
Декодирование инструкции можно написать глядя на крутую таблицу от GoldenCrystal (сайт регулярно лежит, в комментариях к посту есть PDF).
Краткое описание инструкции можно читать в этой markdown-документации. Иногда этого недостаточно, тогда можно читать длинное описание в этой книге.
Самое важная часть - тестирование. Небольшая ошибка в каком-нибудь статусном флаге может привести к катастрофе во время эмуляции. Когда программа большая, ее становится легко сломать в неожиданном месте, поэтому нужны тесты на все инструкции.
Мне очень помогли тесты из этого репозитория. На каждую инструкцию есть 8000+ тестов, которые покрывают все возможные случаи. Суммарно тестов чуть больше миллиона.
Они могут находить даже самые мелкие ошибки - нередко бывает ситуация, что не проходятся ~20 тестов из 8000.
Например, инструкция
MOVE (A6)+ (A6)+ (обращение к регистру A6 делается с пост-инкрементом) должна работать не так, как я реализовал, поэтому я сделал костыль, чтобы работало корректно.Продолжение в комментариях (эмуляция программ)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🔥2❤1🖕1
#opensource
Создание своих плагинов для Vim👩💻
Я не люблю холивары, в том числе на тему текстовых редакторов. У людей есть баг - они рационализируют свой выбор и убеждения.
В худших случаях это вытекает в поиск того, за чей счет бы самоутвердиться, и приписывании другой стороне ложных свойств. Так, многие любители не-Vim уверены, что в Vim нет автокомплита, а многие любители Vim бывают уверены в каких-то вовсе страшных вещах насчет не-Vim.
Я сознательно отказываюсь от рационализации своего выбора. Например, довод о "быстроте редактирования" в Vim неактуален в обстановке, когда программист намного дольше читает уже написанный код или обсуждает решение, чем пишет новый код.
Vim👩💻 (позже его форк Neovim👩💻 ) для меня основной редактор с 2016 года, а полностью сменил гендер "акклиматизировался" я где-то за год (за сравнимое время можно полностью выучить любой другой редактор).
В Vim каждый сам собирает свою "IDE" из 15-20 плагинов и кучи настроек.
Vim я открываю прямо на удаленной машине - потому что работаю в виртуалке.
Для интереса можно сделать свои специфические плагины, чтобы "IDE" стала удобнее. На Neovim плагины можно писать на языке Lua. Чтобы понять, что нужно делать, можно для начала почитать эту и эту доки, но в процессе написания все равно надо много гуглить.
API плагинов мне понравился - если хорошо знать Vim, то в программировании плагинов не надо учить никакие внутренние костыли (другое дело, что в самом Vim полно костылей). Плагин может делать все что угодно многими способами. Добавлять можно новые команды, действия по нажатию хоткеев, колбеки на разные "события" (такие как "сохранение файла" и многие другие).
Сейчас я для работы использую три самодельных плагина, которые очень помогают решать задачи, которые раньше делал ручным способом. Они специфические для компании😁
1️⃣ Ссылка на CodeSearch
🔍 Гифка (10.7 MB)
😱 Исходник
Раньше надо было копировать текст в терминале (выделение мышью и
Это конечно никуда не годится, лучшее решение - встроить CodeSearch например в плагин Telescope😁
По исходнику видно, насколько много костылей в Vim. Например, "слово под курсором" ищется так:
2️⃣ Вставка на Pastebin
🔍 Гифка (13.3 MB)
😱 Исходник
Есть тулза, чтобы отправлять файлы в нечто вроде местного Pastebin. Чтобы вставить кусок логов или кода, приходилось выделять их, делать временный файл, вставлять выделенное, сохранять текст, вызывать тулзу в другой вкладке... Сейчас можно просто выделить текст (или не выделять, если нужно вставить весь файл) и нажать
Видно еще больше костылей: отсутствие многих базовых функций (их надо писать самому), выделенный текст копируется в регистр
3️⃣ Автоматический code style при сохранении
🔍 Гифка (44.1 MB)
😱 Исходник
Используется тулза clang-format для форматирования кода, но с локальными хаками и особенностями. Если код не удовлетворяет код-стайлу, то падает проверка на CI, и приходится запускать тулзу, потом смотреть, что она нормально отработала, закрывать и открывать файл, и так далее... Сейчас эта тулза запускается сама, каждый раз после сохранения файла, меняя только нужные участки кода.
Таким образом, можно написать свои плагины, чтобы уменьшить боль от ручного труда. Это, конечно, базовые плагины, и не сравнятся с монстрами наподобии Telescope или Gitsigns. К "большим" плагинам можно делать свои плагины.😐
Гифки записывал через blue-recorder, визуализация нажатий на кнопки через screenkey.
Создание своих плагинов для Vim
Я не люблю холивары, в том числе на тему текстовых редакторов. У людей есть баг - они рационализируют свой выбор и убеждения.
В худших случаях это вытекает в поиск того, за чей счет бы самоутвердиться, и приписывании другой стороне ложных свойств. Так, многие любители не-Vim уверены, что в Vim нет автокомплита, а многие любители Vim бывают уверены в каких-то вовсе страшных вещах насчет не-Vim.
Я сознательно отказываюсь от рационализации своего выбора. Например, довод о "быстроте редактирования" в Vim неактуален в обстановке, когда программист намного дольше читает уже написанный код или обсуждает решение, чем пишет новый код.
Vim
В Vim каждый сам собирает свою "IDE" из 15-20 плагинов и кучи настроек.
Vim я открываю прямо на удаленной машине - потому что работаю в виртуалке.
Для интереса можно сделать свои специфические плагины, чтобы "IDE" стала удобнее. На Neovim плагины можно писать на языке Lua. Чтобы понять, что нужно делать, можно для начала почитать эту и эту доки, но в процессе написания все равно надо много гуглить.
API плагинов мне понравился - если хорошо знать Vim, то в программировании плагинов не надо учить никакие внутренние костыли (другое дело, что в самом Vim полно костылей). Плагин может делать все что угодно многими способами. Добавлять можно новые команды, действия по нажатию хоткеев, колбеки на разные "события" (такие как "сохранение файла" и многие другие).
Сейчас я для работы использую три самодельных плагина, которые очень помогают решать задачи, которые раньше делал ручным способом. Они специфические для компании
Раньше надо было копировать текст в терминале (выделение мышью и
Ctrl+C), идти на сайт внутреннего поиска кода, вставлять текст Ctrl+V, экранировать спецсимволы... Сейчас можно выделить текст (или не выделять, если нужно найти одно слово под курсором) и нажать Ctrl+S (или ввести команду :CodeSearch, но хоткей быстрее), чтобы вывелась правильная ссылка.Это конечно никуда не годится, лучшее решение - встроить CodeSearch например в плагин Telescope
По исходнику видно, насколько много костылей в Vim. Например, "слово под курсором" ищется так:
vim.fn.expand('<cword>'), а позиции начала и конца выделения это '< и '>.Есть тулза, чтобы отправлять файлы в нечто вроде местного Pastebin. Чтобы вставить кусок логов или кода, приходилось выделять их, делать временный файл, вставлять выделенное, сохранять текст, вызывать тулзу в другой вкладке... Сейчас можно просто выделить текст (или не выделять, если нужно вставить весь файл) и нажать
Ctrl+P (или ввести команду :Paste), чтобы плагин все это сделал и показал ссылку.Видно еще больше костылей: отсутствие многих базовых функций (их надо писать самому), выделенный текст копируется в регистр
" или 0, а путь до текущего файла можно вытащить через vim.fn.expand('%') - это все надо помнить или очень сильно читать документацию.Используется тулза clang-format для форматирования кода, но с локальными хаками и особенностями. Если код не удовлетворяет код-стайлу, то падает проверка на CI, и приходится запускать тулзу, потом смотреть, что она нормально отработала, закрывать и открывать файл, и так далее... Сейчас эта тулза запускается сама, каждый раз после сохранения файла, меняя только нужные участки кода.
Таким образом, можно написать свои плагины, чтобы уменьшить боль от ручного труда. Это, конечно, базовые плагины, и не сравнятся с монстрами наподобии Telescope или Gitsigns. К "большим" плагинам можно делать свои плагины.
Гифки записывал через blue-recorder, визуализация нажатий на кнопки через screenkey.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥2🖕1
#opensource
Обзор исходников Telegram Desktop💬
Для меня Telegram - основное средство связи со всеми коллегами и знакомыми. Поэтому я решил посмотреть в исходники десктопного Telegram и возможно что-нибудь переделать под себя.
Эти исходники находятся тут - https://github.com/telegramdesktop/tdesktop, там же внизу ссылки на инструкции по билду Telegram.
Я попробовал сбилдить Telegram для Linux на двух ноутбуках и на виртуалке, и у меня не получилось, вплоть до того что навернулось окружение рабочего стола на одном ноутбуке😶 Сначала чинил кривые скрипты сборки, потом пытался обновить старую версию Glib, и так далее.
Зато получилось с полпинка сбилдить его для Windows😁 Вспомнил как выглядит Visual Studio: скриншот.
Исходники самого Telegram сравнительно небольшие, но вместе с ним качается и билдится из исходников куча хрени (~25 либ включая ffmpeg и Qt, в размере несколько гигабайтов).
Десктоп написан на движке Qt, с довольно хорошим кодом, он сравнительно небольшой. После исследования известны такие факты:
1️⃣ Что-нибудь "сломать" в самом Телеграме не выйдет, так как десктоп сделан правильно - это всего лишь красивая оболочка над Telegram API, которая получает и посылает все данные из сервера.
Например, когда мы видим статус
2️⃣ Можно написать свой десктоп или даже консольный Телеграм, но на это сомнительное в своей полезности занятие уйдет слишком много времени.
3️⃣ Если собрать "свой" Телеграм и зайти туда хоть раз, то твой аккаунт ставят на счётчик ставят под колпак. За действиями с аккаунта будут следить, в случае разных приколов (спам, накрутка, другой абьюз) будет вечный бан. Об этом честно предупреждают. 🔫
Поменять работу сервера Telegram не выйдет, но можно кастомизировать клиент под свои приколы или под приколы компании, если это рабочий инструмент (вместо такого сизифова труда, как создание своего мессенджера с нуля).
Я сделал такую мелкую фичу: в написанном сообщении обнаруживаются все http-ссылки и заменяются на укороченный вариант из clck.ru. Это может быть нужно для безопасности, если в компании есть свой сервис url shortener для внутренних ссылок, но не хочется тратить по 5-10 секунд для вбивания каждой ссылки.
К счастью, Qt это одна из лучших библиотек C++ на свете, поэтому там есть почти всё свое. Контейнеры (как
Единственный минус - Qt очень заточен под асинхронность, поэтому пришлось сделать какой-то фокус (с 3-го раза), чтобы поставить барьер для ожидания ответов на http-запрос.
✨ Примерный коммит, который включает короткие урлы (с параллельными походами!), выглядит так. (И еще такой кусок кода во "внешней" либе). Там работа со строками.
🔍 😐 Гифка с результатом
Урлы из гифки:
исходник1, исходник2, исходник3
https://clck.ru/34A7Hv, https://clck.ru/3vyXS, https://clck.ru/8f7h6
При желании можно достаточно сильно кастомизировать Telegram, чтобы в нем показывалось больше информации (например, соответствие профиля юзера и его аккаунта во внутренней сети компании, его должность, и т.д.)
Обзор исходников Telegram Desktop
Для меня Telegram - основное средство связи со всеми коллегами и знакомыми. Поэтому я решил посмотреть в исходники десктопного Telegram и возможно что-нибудь переделать под себя.
Эти исходники находятся тут - https://github.com/telegramdesktop/tdesktop, там же внизу ссылки на инструкции по билду Telegram.
Я попробовал сбилдить Telegram для Linux на двух ноутбуках и на виртуалке, и у меня не получилось, вплоть до того что навернулось окружение рабочего стола на одном ноутбуке
Зато получилось с полпинка сбилдить его для Windows
Исходники самого Telegram сравнительно небольшие, но вместе с ним качается и билдится из исходников куча хрени (~25 либ включая ffmpeg и Qt, в размере несколько гигабайтов).
Десктоп написан на движке Qt, с довольно хорошим кодом, он сравнительно небольшой. После исследования известны такие факты:
Например, когда мы видим статус
last seen recently вместо точного времени (есть такая настройка), то десктоп действительно не знает точного времени - сервер отдает ему именно такой статус.Поменять работу сервера Telegram не выйдет, но можно кастомизировать клиент под свои приколы или под приколы компании, если это рабочий инструмент (вместо такого сизифова труда, как создание своего мессенджера с нуля).
Я сделал такую мелкую фичу: в написанном сообщении обнаруживаются все http-ссылки и заменяются на укороченный вариант из clck.ru. Это может быть нужно для безопасности, если в компании есть свой сервис url shortener для внутренних ссылок, но не хочется тратить по 5-10 секунд для вбивания каждой ссылки.
К счастью, Qt это одна из лучших библиотек C++ на свете, поэтому там есть почти всё свое. Контейнеры (как
QVector, QList, QString), регексы, сеть, и очень многое другое. Поэтому такие задачи делаются сравнительно без проблем.Единственный минус - Qt очень заточен под асинхронность, поэтому пришлось сделать какой-то фокус (с 3-го раза), чтобы поставить барьер для ожидания ответов на http-запрос.
Урлы из гифки:
исходник1, исходник2, исходник3
https://clck.ru/34A7Hv, https://clck.ru/3vyXS, https://clck.ru/8f7h6
При желании можно достаточно сильно кастомизировать Telegram, чтобы в нем показывалось больше информации (например, соответствие профиля юзера и его аккаунта во внутренней сети компании, его должность, и т.д.)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20❤5🤡1🖕1
#creepy
Как выбить из C++ настоящий адрес объекта🌃
Недавно коллега искал причины какого-то бага. У нас система многопоточная с большим количеством объектов, поэтому было решено вместе с логами выводить адрес объекта в памяти, к которому относится лог. Это выглядело примерно так:
😁 То есть фильтр логов по адресу показывал явно не все логи, которые должны были быть. После исследования нашли, что такие куски кода:
😒
Класс
Но есть тот факт, что виртуальные классы нормально работают со смещенными указателями. Если указатель когда-то начнет указывать не на тот адрес, то он все равно разрушится по правильному адресу при вызове виртуального деструктора.
😐 Для информации я прочитал две крутые статьи:
C++ vtables - Part 1 - Basics
C++ vtables - Part 2 - Multiple Inheritance
Оттуда узнаем такие факты (верные для 64-битного Linux с включенным rtti):
1️⃣ vtable pointer указывает не на начало vtable, а смещен на 16 байт от начала. Первые 8 байт это число 2️⃣ Компилятор генерирует особые методы под названием thunk, которые фиксят смещение указателя
Продолжение в комментарии - нахождение реального адреса!
Как выбить из C++ настоящий адрес объекта
Недавно коллега искал причины какого-то бага. У нас система многопоточная с большим количеством объектов, поэтому было решено вместе с логами выводить адрес объекта в памяти, к которому относится лог. Это выглядело примерно так:
std::shared_ptr<CallService> callService = ...;Спустя некоторое время оказалось, что созданные объекты пропадают с концами
LOG_INFO("blah blah blah " << callService); // выведет адрес `callService` в конце
void Foo::Bar(std::weak_ptr<IListener> listener) {
LOG_INFO("add listener " << listener.lock().get());
}
где listener это указатель на callService (а класс IListener - предок класса CallService), выводит этот же адрес со смещением, в данном случае было на +4 байта вперед Класс
CallService имел множественное наследование, а из-за этого указатели на базовые типы могли указывать со смещением. На простом примере: struct Foo { int i; };
struct Bar { short s; };
struct Baz { char c; };
struct All : Foo, Bar, Baz {};
// ...
All all; // &all == 0x7ffdfa3ce770
Foo* foo = &all; // foo == 0x7ffdfa3ce770
Bar* bar = &all; // bar == 0x7ffdfa3ce774
Baz* baz = &all; // baz == 0x7ffdfa3ce776
Это логично, потому что указатель должен давать доступ к под-объектам без всяких приколов, то есть вызов bar->s должен работать одинаково, без разницы куда указывает bar. Таким образом, затея коллеги полностью провалилась, полностью.Но есть тот факт, что виртуальные классы нормально работают со смещенными указателями. Если указатель когда-то начнет указывать не на тот адрес, то он все равно разрушится по правильному адресу при вызове виртуального деструктора.
// IEdible - виртуальный класс, (с `virtual ~IEdible()`)То же самое верно для других виртуальных методов. Если взять указатель на предок, и он будет смещенным, то вызов метода все равно отработает корректно, а точнее - неявный параметр
// struct Mango : IFruit, IEdible
std::unique_ptr<IEdible> edible;
{
std::unique_ptr<Mango> mango = std::make_unique<Mango>();
std::cout << mango << std::endl; // вывод "0x56366c90beb0"
edible = std::move(mango);
}
std::cout << edible << std::endl; // вывод "0x56366c90beb8"
// удаляется объект именно по правильному адресу "0x56366c90beb0"!
// с вызовом ~Mango()
this будет пофикшен. struct IEdible {
virtual ~IEdible() = default;
virtual void Eat() = 0;
};
struct Mango : IFruit, IEdible {
void Eat() override { Eaten = true; }; // используется неявный `this`
bool Eaten = false;
};
// ...
IEdible& e = ...; // смещенный указатель
e.Eat(); // работает корректно!
Таким образом можно попробовать найти "настоящий" адрес объекта виртуального класса C++ vtables - Part 1 - Basics
C++ vtables - Part 2 - Multiple Inheritance
Оттуда узнаем такие факты (верные для 64-битного Linux с включенным rtti):
top_offset (смещение относительно реального объекта), вторые 8 байт это указатель на объект typeinfo
this на реальный и потом вызывают нужный метод. В примере выше e.Eat() на самом деле вызовет thunk, который сместит this на -8 (это top_offset) и потом вызовет Mango::Eat().Продолжение в комментарии - нахождение реального адреса!
Please open Telegram to view this post
VIEW IN TELEGRAM
🤯15👍3❤1🖕1
#compiler
Простые тесты для C++ кода на Python🍄 🍄 🍄 🍄 🍄
Часто хочется покрыть тестами код на C++ в проекте - не логику, которую описывает код, а сам исходный код. Сейчас для этого есть приличное количество чекеров в clang-tidy. Можно написать свой чекер, который все равно будут ревьюить много месяцев и КПД всего этого занятия близок к нулю. Для специфических проверок надо что-то колхозить самому.
Примерных проверок может быть много:
1️⃣ Запрет на использование
2️⃣ Запрет на использование старого API в новом коде во время масштабного рефакторинга.
3️⃣ Проверка, что в начале каждого файла находится копипаста лицензии проекта.
Можно придумать проверку на примере лямбд😊 Пусть у нас есть такой код:
Если переменная используется только внутри лямбды, то ее можно вкостылить прямо в capture list. Тогда вместо двух верхних строк примера будет такая строка, дающая аналогичный результат:
Попробуем сделать тест на такие кейсы😁 Если мы для этого используем библиотеки clang, то логика такая:
1️⃣ Получаем AST (Abstract Syntax Tree) из исходного кода.
2️⃣ Лямбды в AST это вершины
3️⃣ "Объявление переменной" в AST это вершина
4️⃣ "Использование переменной" (в каком-то месте) это вершина
5️⃣ Если все "использования переменной" находятся внутри какого-то одного и того же
6️⃣ Делаем рекурсивный обход AST с корня и держанием указателя на "текущую лямбду", и делаем проверку на пункт 5.
Этот тест можно сделать на👩💻 C++: вкостылить новый чекер в локальной сборке clang-tidy или просто сделать программу на libclang. Но можно сделать то же самое на 👩💻 Python и кода будет меньше в несколько раз, и это делается быстрее - менее муторно. Python хорошо подходит для быстрого написания разных тестов.
Поддерживается libclang в Python, и после просмотра примеров можно поставить его себе:
Также не хватает некоторых очевидных фичей, например получения родительской ноды: в курсорах есть пара ссылок на другие курсоры (типа родительские, двух видов), но они работают неправильно.
Кроме тестов можно писать другие тулзы, например "кодогенераторы" - которые сгенерируют какой-нибудь исходник на основе существующего кода. Про кодогенераторы можно почитать лонгрид.
Еще можно делать "исправляторы" исходников - которые берут AST, что-то туда дописывают и сохраняют в другой файл, а компилятор имеет дело с уже поправленным AST (то есть с этим другим файлом).
Простые тесты для C++ кода на Python
Часто хочется покрыть тестами код на C++ в проекте - не логику, которую описывает код, а сам исходный код. Сейчас для этого есть приличное количество чекеров в clang-tidy. Можно написать свой чекер, который все равно будут ревьюить много месяцев и КПД всего этого занятия близок к нулю. Для специфических проверок надо что-то колхозить самому.
Примерных проверок может быть много:
typeid(x).name(), потому что он дает mangled имя, с советом использовать костыль, который отдаст demangled имя.Можно придумать проверку на примере лямбд
int counter = 15;В этом примере лямбда использует внешнюю переменную, которая влияет на логику.
const auto addEvent = [&counter](int number) {
if (counter > 0) {
// do something...
--counter;
}
};
// ... call the lambda
addEvent(1337);
Если переменная используется только внутри лямбды, то ее можно вкостылить прямо в capture list. Тогда вместо двух верхних строк примера будет такая строка, дающая аналогичный результат:
auto addEvent = [counter = 15](int number) mutable {
Такой же подход работает для переменных любых типов. Если интересно, что происходит внутри лямбд, то можно почитать целую книгу про них.Попробуем сделать тест на такие кейсы
LambdaExpr.VarDecl.DeclRefExpr.LambdaExpr, а "объявление переменной" находится вне этого LambdaExpr, то тест должен упасть, потому что данную переменную можно всунуть в capture list лямбды.Этот тест можно сделать на
Поддерживается libclang в Python, и после просмотра примеров можно поставить его себе:
pip install pytestи сделать такой простой тест, где реализуется описанная проверка для лямбд. Можно в директории рядом сохранить тестовый файл source.cpp и проверить, что тест падает:
pip install clang
pip install libclang
python3 -m pytest test.pyВывод:
E Failed: These variables can be declared in lambda capture:libclang на Python выглядит нормально, но неприятно то, что для понятия "нода AST" не к месту придумали новый термин "курсор".
E "counter" (at source.cpp:3:44), to lambda at source.cpp:4:88
Также не хватает некоторых очевидных фичей, например получения родительской ноды: в курсорах есть пара ссылок на другие курсоры (типа родительские, двух видов), но они работают неправильно.
Кроме тестов можно писать другие тулзы, например "кодогенераторы" - которые сгенерируют какой-нибудь исходник на основе существующего кода. Про кодогенераторы можно почитать лонгрид.
Еще можно делать "исправляторы" исходников - которые берут AST, что-то туда дописывают и сохраняют в другой файл, а компилятор имеет дело с уже поправленным AST (то есть с этим другим файлом).
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15🖕1
#youtube
Поваренная книга пирата: создание своего торрент-клиента 🏴☠️
https://youtu.be/usVMq7LDW1Y
Когда есть желание написать о чем-то прикольном, обычно я делаю это в двух форматах:
1️⃣ Статья на Хабр - кондовые лонгриды, которые можно читать, помолясь и перекрестясь.
2️⃣ Этот канал - небольшие приколы, которые быстро читаются.
Сейчас я решил попробовать новый формат - а именно формат видео на ютубе.
Первое видео получилось длиной в полчаса, оно в основном про создание своего торрент-клиента на Python и побочные вещи. Могу сказать, пока я готовил его, я посмотрел с десяток других видео про торренты на ютубе (в том числе типа дофига технические), там близко нет того уровня проникновения, как у меня.
Возможно, такой формат будет более удачным. Так можно накидать неограниченный объем информации (как на Хабре) и оставаться неформальным (как в Телеге)😁
Заранее прошу прощения за мое чувство юмора, и дисклеймер: старайтесь не пиратить, не бухать, не накуриваться; а если очень хочется, то делайте так, чтобы никто не узнал😡
Поваренная книга пирата: создание своего торрент-клиента 🏴☠️
https://youtu.be/usVMq7LDW1Y
Когда есть желание написать о чем-то прикольном, обычно я делаю это в двух форматах:
Сейчас я решил попробовать новый формат - а именно формат видео на ютубе.
Первое видео получилось длиной в полчаса, оно в основном про создание своего торрент-клиента на Python и побочные вещи. Могу сказать, пока я готовил его, я посмотрел с десяток других видео про торренты на ютубе (в том числе типа дофига технические), там близко нет того уровня проникновения, как у меня.
Возможно, такой формат будет более удачным. Так можно накидать неограниченный объем информации (как на Хабре) и оставаться неформальным (как в Телеге)
Заранее прошу прощения за мое чувство юмора, и дисклеймер: старайтесь не пиратить, не бухать, не накуриваться; а если очень хочется, то делайте так, чтобы никто не узнал
Please open Telegram to view this post
VIEW IN TELEGRAM
YouTube
ПОВАРЕННАЯ КНИГА ПИРАТА: СОЗДАНИЕ СВОЕГО ТОРРЕНТ-КЛИЕНТА
Исследуем протокол BitTorrent и делаем свой торрент-клиент на Python с asyncio.
Ссылки:
Канал в Телеграме - t.me/cxx95
Исходный код - github.com/Izaron/pidorrent
Сайт протокола BitTorrent - www.bittorrent.org
Сбор статистики в DHT - andv.medium.com/веселимся…
Ссылки:
Канал в Телеграме - t.me/cxx95
Исходный код - github.com/Izaron/pidorrent
Сайт протокола BitTorrent - www.bittorrent.org
Сбор статистики в DHT - andv.medium.com/веселимся…
🔥25👍2🖕1
#madskillz
Как правильно выполнить любой код до и после main()🏃
Функция
Почти всегда этот код - инициализация статических объектов, которая выполняется до
В этом коде можно делать что угодно (читать файлы, записывать в поток и т.д.), окружение там полностью готово. Я часто использую лямбды:
Пример на godbolt 1, godbolt 2.
😀 Есть крутой лонгрид (со схемами) про то, что происходит до и после
Но все описание там "примерное", потому что поведение зависит от компилятора и его версии🖥
В моем случае некоторые действия находятся в другом положении схемы (увидел по
⌨️ Для того, чтобы "вызвать функцию до
А именно
Здесь слова "constructor" и "destructor" не относятся к классам в C++, это исторические названия еще до C++.
Пример на godbolt с ctor-ами и dtor-ами
Однако здесь есть типичный прикол с😁
👍 Пример по ссылке выше, скомпилированный на
❌ Более старый релиз
😠 Это связано с тем, что
А компилятор данной версии так скомпилировал, что кторы выполняются ДО НЕГО и пытаются обратиться к неинициализированному объекту.
Функция
Мораль думайте сами👍
❔ Для чего может быть полезна такая штука? Лучше описать как "вопрос-ответ"
1️⃣
Вопрос: В чем принципиальное отличие этого кода:
2️⃣
Вопрос: Зачем это все надо, почему нельзя этот кусок кода поместить в
Ответ: Иногда нужно выполнить кусок кода, только если мы линкуем какую-то библиотеку или подключаем хедер. Например, если в библиотеке хэширования есть расчет хитрых данных для хэшей на старте программы в
Да точно это же и происходит для
3️⃣
Вопрос: Какие жесткие проблемы решает эта запись?
Ответ: В кторах и дторах можно указывать приоритет😐 Чем он выше, тем позже выполнится ктор (и соответственно раньше дтор).
Пример на godbolt.
Это решает артиллерийский выстрел в ногу под названием Static Initialization Order Fiasco. Очередность выполнения инициализаций обычно зависит от порядка линковки, а с приоритетом стало можно это нормально разруливать!😎
В остальном проблемы линковки все еще остаются легендарными - можно почитать пример особо опасной проблемы, о которой писал раньше: https://news.1rj.ru/str/cxx95/76
Как правильно выполнить любой код до и после main()
Функция
int main() не является настоящей "точкой входа" в программе. До того, как зайти туда (и после него), программа выполняет много другого кода, в том числе юзерского.Почти всегда этот код - инициализация статических объектов, которая выполняется до
main(). Программа: std::vector<int> MakeVector() {
std::cout << "do MakeVector" << std::endl;
return {1, 2, 3};
}
std::vector<int> VECTOR = MakeVector();
int main() {
std::cout << "do main" << std::endl;
}
Выведет такое:do MakeVectorПример на godbolt
do main
В этом коде можно делать что угодно (читать файлы, записывать в поток и т.д.), окружение там полностью готово. Я часто использую лямбды:
Пример на godbolt 1, godbolt 2.
main(): Linux x86 Program Start Up.Но все описание там "примерное", потому что поведение зависит от компилятора и его версии
В моем случае некоторые действия находятся в другом положении схемы (увидел по
gdb), и еще есть дополнительная вложенность - есть функции для отдельных .cpp-файлов, а там внутри уже вызовы для инициализации переменных.main()", можно пометить ее специальным атрибутом.А именно
__attribute__((constructor)) или менее вырвиглазно [[gnu::constructor]].Здесь слова "constructor" и "destructor" не относятся к классам в C++, это исторические названия еще до C++.
Пример на godbolt с ctor-ами и dtor-ами
Однако здесь есть типичный прикол с
std::cout x86-64 clang trunk по состоянию на 13.06.2023 работает нормально.x86-64 clang 16.0.0 дает сегфолт в кторах и будет работать только если поменять там на printf - ссылка на godbolt.std::cout и подобные объекты инициализируются в конструкторе статического объекта std::ios_base::Init (объявление этого объекта находится в хедере <iostream>).А компилятор данной версии так скомпилировал, что кторы выполняются ДО НЕГО и пытаются обратиться к неинициализированному объекту.
Функция
printf таких спецэффектов не имеет, там просто перенаправление в нужный системный вызов (syscall) без регистрации и СМС.Мораль думайте сами
Вопрос: В чем принципиальное отличие этого кода:
[[gnu::constructor]] void foo() {
// приколы
}
от этого: struct Dummy {
Dummy() { /* приколы */ }
};
Dummy dummy;
или более укуренного этого: const auto dummy = []{
// приколы
return nullptr;
}();
Ответ: Никакого, кроме более удобной записи.Вопрос: Зачем это все надо, почему нельзя этот кусок кода поместить в
int main()???Ответ: Иногда нужно выполнить кусок кода, только если мы линкуем какую-то библиотеку или подключаем хедер. Например, если в библиотеке хэширования есть расчет хитрых данных для хэшей на старте программы в
[[gnu::constructor]], то не надо париться с тем, как и что вызвать в main(), можно просто линковать библиотеку и заработает.Да точно это же и происходит для
std::cout, как мы только что увидели выше!Вопрос: Какие жесткие проблемы решает эта запись?
Ответ: В кторах и дторах можно указывать приоритет
Пример на godbolt.
Это решает артиллерийский выстрел в ногу под названием Static Initialization Order Fiasco. Очередность выполнения инициализаций обычно зависит от порядка линковки, а с приоритетом стало можно это нормально разруливать!
В остальном проблемы линковки все еще остаются легендарными - можно почитать пример особо опасной проблемы, о которой писал раньше: https://news.1rj.ru/str/cxx95/76
Please open Telegram to view this post
VIEW IN TELEGRAM
👍16🤩2😱1🌭1🖕1🆒1
#theory
Замороженные корутины🧊
Am Ende bleib ich doch alleine
Но в конце я остаюсь один
Die Zeit steht still und mir ist kalt
Время остановилось, и мне холодно
Этот пост больше теоретический, чем практический - просто эта тема сложная, чтобы даже иметь какой-то minimal viable product😱
Я много работал с двумя фреймворками, которые со всеми плюсами имели в некоторых местах очень неудобный процесс разработки:
1️⃣ Разработка "тасок" по триггеру
"Таска" это такая микро-программа на Python, которая должна запускаться по какому-то триггеру и делать что-то обычно небольшое: отправка email, создание тикетов, http-запрос в сервис, и прочее👦 Сервис выполняет эти программы на каком-нибудь свободном сервере.
Иногда таска может быть посложнее - например, создать и запустить другие таски и ждать их выполнения. В таких случаях исходная таска "замораживается" (переходит в состояние "Wait") и прекращает свою работу, а после финиширования дочерней таски возобновляет работу, снова на каком-нибудь свободном сервере🖥
Есть проблема - система не может как бы "продолжить" код таски с той точки, где было ожидание таски. Был придуман нереальный костыль, который выглядит примерно так:
В "контексте" можно ставить флажки, чтобы имитировать разные стадии таски, еще туда можно сохранять списки/строки/числа✍️
Наконец, оповещение сервису о том, что текущей таске надо дождаться другую таску, делается через бросание исключения (🤔
При таком подходе надо очень аккуратно писать код и много думать, если таске нужно делать что-то немного нетривиальное: например запуск дочерних тасок в цикле, пока одна из них не завершится успешно🙁 Попробуйте сами наколхозить такое, будет треш.
2️⃣ Микросервисы в Apphost
По ссылке открыто описано что такое Apphost - фреймворк для микросервисов (на Python или C++).
Он решает многие старые проблемы, но появляется новая проблема - теперь программа не может просто так сделать HTTP-запрос.
Нужно разбивать всю программу на микроскопические куски кода, которые принимают некие "входящие" объекты и формируют "исходящие", но не более того. Все походы во внешние сервисы делает Apphost. Эти куски кода выполняются на разных серверах.
Та часть, которая раньше делалась мгновенно, сейчас делается в 10 раз медленнее из-за настройки конфигов, жесткого обдумывания "графа" выполнения, и так далее.
❓ Использование корутин
Неприятности в сервисах, указанных выше, теоретически можно решить через корутины. Я когда-то писал о них в канале. Это функции, которые могут приостанавливать свое выполнение, а затем продолжать с той же точки.
На самом деле, проще всего изучать корутины в простых языках - здесь статья про корутины в Lua. Общий подход не меняется.
Потом здесь можно почитать, как корутины реализованы в C++ (начиная с C++20) - это очень сложные статьи, их можно читать несколько недель, зато появляется максимальное понимание, как компилятор преобразовывает код.
Суть идеи, которая решит проблемы, которые я описал - замороженные корутины: программа выполняется на одном сервере, потом останавливается, ожидает возобновления, и выполняется на каком-нибудь другом сервере с той точки, где была остановлена😎
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Замороженные корутины
Am Ende bleib ich doch alleine
Но в конце я остаюсь один
Die Zeit steht still und mir ist kalt
Время остановилось, и мне холодно
Этот пост больше теоретический, чем практический - просто эта тема сложная, чтобы даже иметь какой-то minimal viable product
Я много работал с двумя фреймворками, которые со всеми плюсами имели в некоторых местах очень неудобный процесс разработки:
"Таска" это такая микро-программа на Python, которая должна запускаться по какому-то триггеру и делать что-то обычно небольшое: отправка email, создание тикетов, http-запрос в сервис, и прочее
Иногда таска может быть посложнее - например, создать и запустить другие таски и ждать их выполнения. В таких случаях исходная таска "замораживается" (переходит в состояние "Wait") и прекращает свою работу, а после финиширования дочерней таски возобновляет работу, снова на каком-нибудь свободном сервере
Есть проблема - система не может как бы "продолжить" код таски с той точки, где было ожидание таски. Был придуман нереальный костыль, который выглядит примерно так:
def on_execute(self):Сервис сохраняет "контекст" между разными запусками таски, а метод
if not self.Context.config_stage:
self.Context.config_stage = True
param1 = ...
param2 = ...
task = sdk.CreateTask(param1, param2, ...) # создаем дочернюю таску №1
raise sdk.WaitTask(task)
if not self.Context.run_stage:
self.Context.run_stage = True
param = ...
task = sdk.CreateTask(param, ...) # создаем дочернюю таску №2
raise sdk.WaitTask(task)
on_execute каждый раз запускается сначала ♻️В "контексте" можно ставить флажки, чтобы имитировать разные стадии таски, еще туда можно сохранять списки/строки/числа
Наконец, оповещение сервису о том, что текущей таске надо дождаться другую таску, делается через бросание исключения (
raise), чтобы это "прорвалось" за пределы def on_execute (удобнее, чем просто return, если вызываются всякие вложенные методы) При таком подходе надо очень аккуратно писать код и много думать, если таске нужно делать что-то немного нетривиальное: например запуск дочерних тасок в цикле, пока одна из них не завершится успешно
По ссылке открыто описано что такое Apphost - фреймворк для микросервисов (на Python или C++).
Он решает многие старые проблемы, но появляется новая проблема - теперь программа не может просто так сделать HTTP-запрос.
Нужно разбивать всю программу на микроскопические куски кода, которые принимают некие "входящие" объекты и формируют "исходящие", но не более того. Все походы во внешние сервисы делает Apphost. Эти куски кода выполняются на разных серверах.
Та часть, которая раньше делалась мгновенно, сейчас делается в 10 раз медленнее из-за настройки конфигов, жесткого обдумывания "графа" выполнения, и так далее.
Неприятности в сервисах, указанных выше, теоретически можно решить через корутины. Я когда-то писал о них в канале. Это функции, которые могут приостанавливать свое выполнение, а затем продолжать с той же точки.
На самом деле, проще всего изучать корутины в простых языках - здесь статья про корутины в Lua. Общий подход не меняется.
Потом здесь можно почитать, как корутины реализованы в C++ (начиная с C++20) - это очень сложные статьи, их можно читать несколько недель, зато появляется максимальное понимание, как компилятор преобразовывает код.
Суть идеи, которая решит проблемы, которые я описал - замороженные корутины: программа выполняется на одном сервере, потом останавливается, ожидает возобновления, и выполняется на каком-нибудь другом сервере с той точки, где была остановлена
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14

