Rust счетчик ресурсов как работает

Обновлено: 02.07.2024

Вторая часть про работу с кучей, устанавливаем свой глобальный аллокатор. В принципе, это делается в несколько строк, но наша цель — действовать сознательно и профессионально, с глубоким проникновением в суть явлений, так что легкой жизни не будет (как и обещал).

static и const:: Деструктор

Как утверждает отличное старое руководство, метод Drop::drop() для static переменных может быть реализован, но он не вызывается.

static и const:: Что использовать?

Прекрасный ответ дает RFC-0246:

  • constants declare constant values. These represent a value, not a memory address. This is the most common thing one would reach for and would replace static as we know it today in almost all cases.
  • statics declare global variables. These represent a memory address. They would be rarely used: the primary use cases are global locks, global atomic counters, and interfacing with legacy C libraries.

Для замены глобального аллокатора осталось освоить немного: тип unit, unit-like структуры, и значения параметров типа по умолчанию.

static и const

В японском зоопарке родился слоненок — казалось бы, при чем тут Лужков? Глобальный аллокатор, static и const?! — Спокойно, действуем по ранее утвержденному плану. Да и все равно изучать.

Рассмотрим такой пример:

Каковы сходства и различия между разными "точками", в особенности между static STATIC_POINT и const CONST_POINT ?

Тип unit

In the area of mathematical logic and computer science known as type theory, a unit type is a type that allows only one value (and thus can hold no information).

In Haskell and Rust, the unit type is called () and its only value is also (), reflecting the 0-tuple interpretation.

In Java, the unit type is called Void and its only value is null.

Unit type

Итак, пустой кортеж (tuple) () означает одновременно и тип, и литерал (фиксированное значение). Можно писать так:

  • unit похож на void в C , но там у void нет значения
  • Запись HashMap<T, ()> означает словарь, у которого информацию несут только ключи (как бы HashSet )

static и const:: Адрес

Результат какой-то такой:

  • Взятие адреса изменяемой глобальной статической переменной является опасной операцией
  • print_pointer() с параметром label введена для удобства работы с ассемблерным листингом

Ну т.е. вроде как отдельные адреса заведены под все значения. Стоит ли полагаться на то, что адрес const-значения ( L__unnamed_2 ) постоянен? Как водится, внятное описание мы найдем в "старом учебнике":

More specifically, constants in Rust have no fixed address in memory. This is because they’re effectively inlined to each place that they’re used. References to the same constant are not necessarily guaranteed to refer to the same memory address for this reason.

Нет, не стоит. Если нужен гарантированный глобальный адрес, используем static .

Unit-like структуры

Unit-like структуры, как понятно из названия, также представляют собой и тип, и литерал:

Для комплекта возьмем еще std::alloc::System (мы потихоньку подбираемся к глобальному аллокатору) и напечатаем их адреса:

  • Ну и дела, они все равны!
  • Если раскомментировать // println. , то получаются два уникальных значения для адресов
  • Если нужен гарантированный адрес, то наверняка нужно использовать static

Ассемблер намекает, что MyUnitStruct и System трактуются как const , а MY_UNIT_STRUCT* — как static :

Это все, конечно, интересно, но каково же практическое применение? Еще немного теории, и приступим к практике.

▍ Написание программы

Итак, мы закончили с подготовкой. Что делать теперь? Теперь всё просто. Здесь можно брать документацию Windows API и искать то, что нам интересно.

Первое — функция main теперь должна возвращать windows::Result<()>. В данном случае мы будем возвращать пустой кортеж, но так как мы находимся в Windows, мы можем вернуть очень много вариантов значений. Для тех, кому это может понадобиться, Result принимает error, подробности здесь.


Второе — все вызовы самих Windows API должны производиться через директиву unsafe.

Я видел множество каких-то непонятный священных войн по поводу того, что использование директивы unsafe должно быть запрещено законом. Если честно, я никогда не понимал из-за чего происходят подобные войны. Давайте посмотрим на официальную документацию. Здесь нам ясно и чётко рассказывают, что в использовании unsafe нет ничего дьявольского, и оно разрешено Женевской конвенцией. Вам просто необходимо знать, что да как использовать.

Ок. Лезем дальше. Давайте откроем документацию Microsoft и найдём в ней MessageBoxA.

У нас есть четыре параметра, которые нам надо передать этой функции.

  • hWnd — дескриптор основного окна, в котором будет показано уведомление. Так как MessageBoxA — это модальное окно, то выполнение команд в основном окне заблокируется, пока будет активно уведомление. Так как у нас нет никакого окна, то сюда можно смело передавать NULL.
  • lpText/lpCaption — указатель типа Long на строку, строки, которые будут показывать в заголовке и теле окна.
  • uType — набор констант, которые зададут поведение, тип кнопок и иконок в этом MessageBox


Как вы видите, нам не пришлось изобретать велосипед, и пытаться создавать тип NULL, которого не существует в Rust, да и передача строк не требует шести преобразований их в разные форматы. Поэтому нам не нужно извращаться и писать что-то типа:


Всё достаточно цивильно.

Так, давайте подключим наши биндинги, соберём программу и посмотрим, как она работает. Конечный результат выглядит вот так:


Замечательно! Повсюду кракозябры, как в 1999 году. Что делать? Разбираемся и понимаем, что MessageBoxA работает c ANSI строками, в то время как MessageBoxW работает с Unicode (Wide strings). Собственно говоря, эта нотация сохранилась во всех Windows API. Я не знаю причин не использовать W версии функций. Но, будьте аккуратны, большое количество писателей мануалов на английском языке не понимают разницы, и вы увидите загруженность A версиями функций, в то время как вам необходимо использовать W версии этих функций.

Заменяем всё в lib.rs и main.rs. Пробуем ещё раз.


При весе в 164 килобайт на диске, программа занимает 940 килобайт оперативной памяти. И при этом работает с Windows API. Достаточно скромно. Лучше, чем ассемблер с кривыми биндингами, который работает только в консоли.

Итак, давайте пройдёмся по основным моментам этой главы:

  1. Вызов WinAPI это всегда unsafe вызов.
  2. fn main должна возвращать результат, понятный Windows.
  3. Вы знаете где и как найти документацию по Windows API и можете использовать их в вашей программе. (Все данные о WinAPI можно достать здесь, а данные о функциях, спортированных в rust находятся здесь.

▍ Что дальше?

И всё это доступно прямо сейчас в rust. Надеюсь вы узнали для себя немного нового и возможно у вас появились идеи о том, как вы могли бы это применить в вашей работе. Что же, если это так, то я очень рад, что вам пригодилось.

И, как обычно, конкурс. На этот раз задача будет отстоять в реализации самой компактной игры 2048 написанной на rust. Первое место — эквивалент $25 в пейпал. Второе место — $15, третье $5.

Мерять будем потребление оперативки. Игра должна быть написана на rust и в ней обязательно использовать winapi. Она должна быть графической. Но конкретный движок на выбор.

Значения параметров типа по умолчанию

Мы почти у цели, осталось немного. Допустим, мы сделали сервер с методами start() , stop() , handle_request() и запускаем его так:

Все работает, но в процессе эксплуатации понимаем, что println!() надо бы заменить вызовами некоего нормального логгера, и Server у нас теперь выглядит вот так:

Мы поменяли класс Server , также пришлось поменять и код, который его использует, т.е. функцию run_server() . Если такого кода много, возникнет много проблем, избежать их позволяют значения параметров типа по умолчанию.

Но сперва надо где-то взять реализацию Logger. Если нет желания создавать экземпляр, можно использовать как "носитель" любую unit-like структуру в области видимости, например, реализуем Logger для std::alloc::System:

  • "Навесить" методов можно на что угодно, не только на "свое"
  • Ключевым моментом является то, что не требуется создавать экземпляр реализатора — он уже есть (как выяснилось, один на все unit-like структуры) и передается как первый параметр ( &self )
  • voila — функция run_server() вернулась к первоначальному виду, что и требовалось
  • PS: DarkEld3r дал ценный комментарий, действительно, необходимо отметить, что в качестве "значения по умолчанию" можно использовать любые, а не только unit-like структуры, например: struct Server<L: Logger = MyLogger>

Может создаться впечатление, что здесь происходит процесс, известный как "Внедрение зависимости" (Dependency Injection), т.е. код:

… выступает в роли "внедренца" и связывает экземпляр типа Server с интерфейсом Logger . Нет, за кулисами генерируются разные "классы" Server под каждую конкретную реализацию Logger .

Этот момент чрезвычайно важен, для его понимания сделаем второй логгер:

… и заглянем за кулисы:

  • Налицо две версии класса кодовой базы Server , по одной для каждого используемого логгера
  • Никакого DI, конкретный вариант "класса" Server точно "знает", с каким логгером он работает
  • Такая "механика" работает быстрее, чем вызов через vtable, но при этом исполняемый файл "раздувается" в размере

Замена глобального аллокатора

Замена глобального аллокатора описана здесь, так заменим:

System.alloc() , где это и что это? Про сам std::alloc::System мы уже знаем, это pub struct System; а откуда взялось System.alloc() ? Вопрос интересный, как уже понятно, навесить alloc() на unit-like структуру System можно где угодно (в пределах видимости).

Реализация зависит от платформы, "условная компиляция", видимо, происходит здесь:

▍ Введение

Для тех кто незнаком с Rust — вам необходимо будет подтянуть свои знания. Хотя бы потому что с момента своего создания в 2010 году язык завоёвывает популярность. С 2016 года язык появлялся в отчётах stackoverflow в списках самых любимых языков. В 2021 году Rust остаётся самым любимым языком на stackoverflow, что не мешает ему оставаться очень нишевым языком, доступным только избранным. В основном потому, что сам по себе Rust это низкоуровневый язык, который хоть и красив и приятен, но всё же учится не за 20 минут по видео из ютуба.

Rust используется в ядре Linux. Естественно, так как Rust был изначально создан в Mozilla Foundation, то большое количество компонентов Firefox написано именно на нём. В Microsoft решили, что не стоит отставать, и начали использовать Rust в части своих проектов.


И если год назад вы могли увидеть в репозитории упоминания, что проект не находится в стабильном состоянии и не рекомендуется к использованию в работе, то сейчас эти упоминания из проекта пропали. (Хотя, для того чтобы вы могли воспользоваться результатами этого проекта, вам всё равно придётся установить Rust Nightly билд, и вы успеете насмотреться на предупреждения от компилятора).

Итак, в чём заключается проект? Все Windows API импортированы в Rust и вы можете использовать любые стандартные функции прямо из вашей программы, без большого количества колдовства.

static и const:: Встраивание (Inlining)

". 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).

Rust счетчик ресурсов как работает

Сервера RUST "ZERO ZONE"

Также справа от слотов есть кнопки быстрого вызова команд:
( Kit, INFO, RPG, RULES, VIPBox, STATUS )

Сервера RUST "ZERO ZONE"

/home add ИМЯдома — сохранить точку ТП домой ( максимально можно сохранить 5 точек );
/home ИМЯдома — Телепортироваться в указанный дом (ранее сохранённый);
/home list — Показать весь список сохранённых точек Телепортации домой;
/home remove ИМЯдома — Удалить указанную точку Телепортации домой;

У нас на серверах можно ставить 5 точек для сохранения Домов. Откат (кулдаун) между телепортациями составляет 5 минут, а время ожидания ТП после отправки команды - 5 секунд.
В день можно делать неограниченное количество телепортаций как Домой, так и к Друзьям.

/town - Случайная точка для телепортации игрока по координатам (по умолчанию - отключена), но Администратор может назначить любое место для телепортации (как правило это точка на карте на верху какого-либо "РадТауна").

static и const:: Инициализация

Ну и, наконец, общим для всех этих… ээээ… "переменных" является то, что функции, их инициализирующие, должны быть объявлены с квалификатором const :

  • Функции такого типа являются чистыми (pure), результат их известен во время компиляции и вызов функции может быть заменен ее результатом
  • Соответственно, в "кучу" лезть из таких функций нельзя

▍ Пишем программу

Ну что же, теперь мы умеем создавать программу на rust, которая может тянуть WinAPI. Дело за малым — создать окно, понакидать в него что-то, что будет представлять тайлы в нашей игре, и написать логику.

Ну что же, давайте создадим окно и понакидаем в него всякого разного.

Для начала, давайте обновим build.rs и добавим кое-какие функции, которые нам понадобятся.


После этого заменим импорты и код Main на следующий метод:


Здесь мы вызываем GetModuleHandleA. Эта функция возвращает нам называние модуля в котором мы работаем. Каждое окно должно быть привязано к определённому модулю. В данном случае это будет название нашего бинарного файла.

После этого создаём структуру WNDCLASSA и заполняем её параметрами, с которыми наше окно будет запускаться.

Далее, мы регистрируем этот класс.

После чего мы запускаем это окно, вызывая CreateWindowExW.

Компилируем и пробуем всё это запустить.

На экране нет ничего. Более того, если мы попробуем перекомпилировать проект, то у нас всё рухнет и ругнётся на линкер с ошибкой, что конечный бинарный файл для записи недоступен.

Идём в диспетчер задач и видим там, что наша программа безбожно висит.

Сколько раз вам приходилось видеть вот такое?


Именно поэтому наша программа превратилась в программу-невидимку.

Первым делом добавляем после CreateWindowExW следующий код


А в описании структуры WNDCLASSA заменим:

Теперь давайте запустим и проверим всё это.

Ура! Наконец-то! У нас есть окно!


Только не пытайтесь его закрыть. У вас всё рухнет. Окно закроется, а программа останется висеть в памяти. И не пытайтесь изменять размер этого окна. Если вдруг чего, вы увидите чёрные прямоугольники, вместо контента.

Обновляем код и получаем:


При закрытии окна мы выходим из программы, а при изменении размера мы его перерисовываем. Теперь окно не будет виснуть, и вы можете запустить и закрыть программу.

Проверяем. Бинарник размером в 160 килобайт, занимает 1 мегабайт оперативной памяти.

Проверяем знания:

▍ Начало работы

Итак, что же нам нужно сделать, для того чтобы начать работать с Windows API в Rust.

Для начала, нам потребуется rust nightly.

rustup default nightly
rustup update

После этого вам нужно будет создать новый проект на rust и сразу создать дополнительную библиотеку под названием bindings.
cargo new testproject
cd testproject
cargo new --lib bindings .
И, после этого добавить в cargo.toml в нашем проекте зависимости для подключения библиотеки Microsoft.

[dependencies]
bindings = < path = "bindings" >
windows = "0.21.1"

На момент написания статьи, актуальная версия библиотеки 0.21.1, посему везде будем использовать именно эту версию.

После этого в самой папке с библиотекой bindings нам нужно будет добавить в Cargo.toml следующий текст:
[package]
name = "bindings"
version = "0.1.0"
edition = "2018"

[dependencies]
windows = "0.21.1"

[build-dependencies]
windows = "0.21.1"
Итак, что у нас тут получается? У нас есть проект под названием testproject, в нём есть библиотека bindings. Цель этой библиотеки — подключать зависимости для того, чтобы вы могли работать с Windows API в вашем приложении.

Сам файл bindings/src/lib.rs будет состоять из одной команды:


Это — вызов макроса, который подключит все необходимые зависимости.

И теперь, самое интересное, файл bindings/build.rs

Прокачиваем силу — Rust и Windows API


Недавно я написал статью Трясём стариной — или как вспомнить Ассемблер, если ты его учил 20 лет назад. В статье рассказывается о том, как изучать ассемблер на примере игрушки 2048. Возможно для целей самой статьи игрушка была подходящая, но конечный результат меня немного удручил. Бинарник размером в 10 килобайт, который потребляет 2 мегабайта памяти, из-за неправильно слинкованной библиотеки резал глаза.

Посему я задался вопросом, а как это можно было бы сделать правильнее? Наверняка есть намного более удачное решение. (И организовал ещё один конкурс с призами в конце статьи)

А почему бы не сделать на Rust, и правильно прикрученных библиотеках? При этом, если вы знаете, что делаете, то вы можете запросто уменьшить количество потребляемой оперативной памяти, но при этом написать визуальную игрушку с использованием Windows API.

Причём это не значит, что вы будете использовать какую-то нестандартную библиотеку. Встречайте — windows-rs, проект поддерживаемый Microsoft. Ваш билет в мир Windows, если вы пишете на Rust.

При написании этой статьи я понял, что мы берём понемногу из двух миров — Windows API и rust. Определённые моменты будут казаться очевидными для rust разработчиков, другие моменты будут казаться очевидными для Windows разработчиков. Так как это — статья для всех, то я решил что будет лучше, если объясню больше, чем меньше.

Rust счетчик ресурсов как работает

RUST - Электрический счетчик (Counter)

Про то, как работает электрический счётчик в игре Раст. Полный разбор принципа работы счетчика и варианты .

Монитор ресурсов в Rust позволяет просматривать содержимое шкафа и больших ящиков через приложение Rust или .

[Rust/Раст] - Полный гайд по электричеству

Таймкоды: 1:09 - общая информация об электричестве 1:19 - генератор (small generator) 1:37 - ветряк (wind turbine) 2:11 .

static и const:: Изменяемость (Mutability)

Ни одно из "значений" не может быть изменено в безопасном режиме, по разным причинам:

MUT_STATIC_POINT можно изменить в unsafe:

  • static mut являет собой глобальную переменную, каковые часто рассматриваются как глобальное зло, а с такими вещами нужно работать осторожно, огораживая и помечая опасные участки при помощи unsafe .

Попытки изменить STATIC_POINT или CONST_POINT заканчиваются ужасами Segmentation fault:

Читайте также: