Вторая часть про работу с кучей, устанавливаем свой глобальный аллокатор. В принципе, это делается в несколько строк, но наша цель — действовать сознательно и профессионально, с глубоким проникновением в суть явлений, так что легкой жизни не будет (как и обещал). В японском зоопарке родился слоненок — казалось бы, при чем тут Лужков? Глобальный аллокатор, static и const?! — Спокойно, действуем по ранее утвержденному плану. Да и все равно изучать. Рассмотрим такой пример: Каковы сходства и различия между разными "точками", в особенности между static STATIC_POINT и const CONST_POINT? Для всех значений можно получить сырой указатель и напечатать его значение: Результат какой-то такой: Посмотрим в ассемблер, благо чудесный play.rust-lang.org это позволяет: Ну т.е. вроде как отдельные адреса заведены под все значения. Стоит ли полагаться на то, что адрес const-значения (L__unnamed_2) постоянен? Как водится, внятное описание мы найдем в "старом учебнике": Нет, не стоит. Если нужен гарантированный глобальный адрес, используем static. "...they’re effectively inlined" — ой ли? Любую документацию по Rust надо рассматривать либо как неполную, либо как противоречивую (собственно, Гёдель), а в The Rust Reference авторы сами пишут красными буквами: Так что будем проверять все, что можно проверить, благо, инструментарий имеется. Погрузимся в глубины присваивания, напрямую можно присваивать только const: Посмотрим в ассемблер вот такого кода: Ну, такое себе "встраивание" — сначала все копируется в ecx и eax, затем оттуда в стек (локальную переменную p1). В чем дело? А, так это режим Debug, а что в Release?: Вот тут, действительно, встраивание. 257698037810 это аккурат (60_i64 << 32) + 50, т.е. упакованное значение для const CONST_POINT: Point = new_point(50, 60). Но постойте, мы же можем и static присвоить, для этого надо реализовать Copy: Вот что выходит: Тоже inlining, (20_i64 << 32) + 10 = 85899345930 как раз соответствует static STATIC_POINT: Point = new_point(10, 20). Короче, разницы, в плане "встраивания", между static и const на нашем примере не видно (понятно, что с поправкой на то, что Copy is-a-must). Код примеров тут. Ни одно из "значений" не может быть изменено в безопасном режиме, по разным причинам: MUT_STATIC_POINT можно изменить в unsafe: Попытки изменить STATIC_POINT или CONST_POINT заканчиваются ужасами Segmentation fault: Ну и, наконец, общим для всех этих… ээээ… "переменных" является то, что функции, их инициализирующие, должны быть объявлены с квалификатором const: Как утверждает отличное старое руководство, метод Drop::drop() для static переменных может быть реализован, но он не вызывается. Прекрасный ответ дает RFC-0246: Для замены глобального аллокатора осталось освоить немного: тип unit, unit-like структуры, и значения параметров типа по умолчанию. Итак, пустой кортеж (tuple) () означает одновременно и тип, и литерал (фиксированное значение). Можно писать так: Или так: Или так: Unit-like структуры, как понятно из названия, также представляют собой и тип, и литерал: Для комплекта возьмем еще std::alloc::System (мы потихоньку подбираемся к глобальному аллокатору) и напечатаем их адреса: Ассемблер намекает, что MyUnitStruct и System трактуются как const, а MY_UNIT_STRUCT* — как static: Это все, конечно, интересно, но каково же практическое применение? Еще немного теории, и приступим к практике. Мы почти у цели, осталось немного. Допустим, мы сделали сервер с методами start(), stop(), handle_request() и запускаем его так: Все работает, но в процессе эксплуатации понимаем, что println!() надо бы заменить вызовами некоего нормального логгера, и Server у нас теперь выглядит вот так: Мы поменяли класс Server, также пришлось поменять и код, который его использует, т.е. функцию run_server(). Если такого кода много, возникнет много проблем, избежать их позволяют значения параметров типа по умолчанию. Но сперва надо где-то взять реализацию Logger. Если нет желания создавать экземпляр, можно использовать как "носитель" любую unit-like структуру в области видимости, например, реализуем Logger для std::alloc::System: Теперь System можно использовать как значение по умолчанию в параметрах типа: Может создаться впечатление, что здесь происходит процесс, известный как "Внедрение зависимости" (Dependency Injection), т.е. код: … выступает в роли "внедренца" и связывает экземпляр типа Server с интерфейсом Logger. Нет, за кулисами генерируются разные "классы" Server под каждую конкретную реализацию Logger. Этот момент чрезвычайно важен, для его понимания сделаем второй логгер: … и заглянем за кулисы: Замена глобального аллокатора описана здесь, так заменим: System.alloc(), где это и что это? Про сам std::alloc::System мы уже знаем, это pub struct System; а откуда взялось System.alloc()? Вопрос интересный, как уже понятно, навесить alloc() на unit-like структуру System можно где угодно (в пределах видимости). Через std::alloc::System реализуется std::alloc::GlobalAlloc: Реализация зависит от платформы, "условная компиляция", видимо, происходит здесь: Вот реализация для windows: allocate(): Теперь посмотрим, как работает std::alloc::alloc() из предыдущей серии. std::alloc::alloc(): __rust_alloc(): С #[global_allocator] все понятно, идем в __rdl_alloc(): Вот и все, доплыли до знакомого System.alloc(). Еще глянем RawVec, и на этом закончим. RawVec: trait std::alloc::Allocator: struct std::alloc::Global: impl Allocator for Global: Global.alloc_impl(): Далее пойдем только в ветку с alloc(layout): Не прощаюсь. Далее: Замыкания