Rust как разделить ресурсы на 3 части

Обновлено: 14.05.2024

Этот цикл статей является вольным переводом книги «Rust by Example», которую пишет Хорхе Апарисио на Github.

На момент написания этого топика автор книги создал 49 глав, в первой части будет перевод первых пяти. Убедитесь, что Rust установлен и под рукой имеется документация.

Содержание
1. Привет, мир!

Это код традиционной программы «Hello World»:

println! это макрос (мы рассмотрим их позже), который печатает текст в консоль.

Программа может быть сгенерирована с помощью компилятора Rust rustc:

rustc создаст бинарный файл «hello», который можно запустить:

2. Форматированный вывод

Макрос println! не только выводит в консоль, а также способен форматировать текст и сериализованные значения. Корректность проверяется во время компиляции.


Дополнительная информация о форматировании здесь: std​::fmt​

3. Литералы и операторы

Целые числа 1, с плавающей точкой 1.2, символы 'a', строки "abc" , логические true и значения пустого типа () могут быть выражены с помощью литералов.

Также целые числа можно выразить через шестнадцатеричное, восьмеричное или двоичное обозначение, используя один из префиксов: 0x, 0o или 0b.

В числовые литералы можно вставлять подчёркивания для читабельности, например, 1_000 такой же, как и 1000, а 0.000_001 такой же, как и 0.000001.

Мы должны сказать компилятору, какой из литералов мы используем. Сейчас мы будем использовать суффикс u , указывающий, что литерал является целым числом без знака, суффикс i чтобы указать, что это знаковое целое число. Мы рассмотрим систему типов в 5 главе, а также подробную информацию о аннотировании литералов.

Доступные операторы и их приоритет похож на C-подобных языках.

4. Переменные

Значения (как и литералы) могут быть связаны с переменными, используя обозначение let.

4.1 Изменяемость
4.2 Области и видимость

Переменные имеют локальную область, и имеют видимость в блоке (блок представляет собой набор операторов, заключённых в фигурные скобки <>). Кроме того, допускается скрытие переменной.

4.3 Предварительное объявление

Можно сперва объявлять переменные, а инициализировать их позже. Но эта форма редко используется, так как это может привести к использованию неинициализированных переменных.


Компилятор запрещает использование неинициализированных переменных, так как это привело бы к непредсказуемым последствиям.

5. Типы

Rust обеспечивает безопасность с помощью статической проверки типов. Тип переменной может быть явно указан при её объявлении. Тем не менее, в большинстве случаев, компилятор сможет определить тип переменной из контекста, если это очевидно.

  • целые числа: i8 , i16 , i32 , i64 и int (размер зависит от платформы)
  • целые числа без знака: u8 , u16 , u32 , u64 и uint (размер зависит от платформы)
  • с плавающей точкой: f32 , f64
  • char значения Unicode: 'a', 'α' и '∞' (4 байта каждый)
  • bool true или false
  • кортежи ()
5.1 Приведение типов

Rust не предоставляет неявного преобразования типов (coercion) между примитивами, но, явное приведение типов (casting) может быть достигнуто с помощью ключевого слова as.

5.2 Литералы

В числовых литералах тип может быть аннотирован, добавив тип в качестве суффикса, за исключением uint , использующей суффикс u и int , который использует суффикс i .

  • fun(&foo) используется, чтобы передать аргумент в функцию по ссылке, а не по значению fun(foo) .
  • std::mem::size_of_val является функцией, но вызывается с указанием полного пути. Код можно разделить на логические единицы, называемые модулями. Здесь функция size_of_val определена в модуле mem , а модуль mem определен в крэйте std .
5.3 Логический вывод

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

Отсутствует необходимость в аннотации типа переменной, компилятор счастлив как и программист!

5.4 Псевдонимы (алиасы)

Оператор type может быть использован, чтобы задать новое имя существующему типу. Тип должен быть в стиле CamelCase, либо компилятор выдаст предупреждение. Исключением из этого правила являются примитивные типы: uint , f32 и другие.


Основное применение псевдонимов это снижение количества кода, например, тип IoResult является псевдонимом типа Result<T, IoError> .

Разделение ресурсов в Rust

Он, ясно дело, не компилируется, так как сокет заимствован потоком обработчика, когда я пытаюсь его закрыть.
В сях я бы просто прихлопнул сокет и вышел, тут же все, видимо, не так просто.
Как закрыть сокет при получении SIGTERM, если в этот момент поток владеющий им заблокирован вызовом recv_from?



Он же должен сам закрыться по вызову деструктора.


Открытый сокет в системе и невозможность перебиндиться при следующем запуске приложения, говорят мне об обратном.


Прекрасный язык, настолько безопасный, что даже блокирующие вызовы использовать нельзя с безопасным освобождением ресурсов :)

Смешно, но, видимо придется использовать асинхронные сокеты.

А зачем ты его тогда используешь, раз он такой говенный?


Попробуй сделать копию сокета в main.

Вот это знатные костыли! =)))

Костыль - это то, что в потоке не вызывается деструктор, т.к. поток не принимает сигнал SIGTERM.


О, спасибо.
Не дочитал доку.
Решено.

Если подскажете как такое поведение реализовать вручную (для своих структур), не используя unsafe, буду очень благодарен

Когда UdpSocket'у добавят метод shutdown, всё станет хорошо: try_clone перед отдачай сокета в поток, потом shutdown на экземпляре, который остался во владении у главного потока, как это бы делалось в случае TCP.


Ну, в принципе, drop делает то же самое, только через деструктор, разве нет?
Ксатати, я немного не понимаю: drop в данном случае захватывает UdpSocket по значению, и при выходе из скопа внутри drop, объект разрушается, а ресурсы освобождаются.
Но разве на него не будет при этом указывать ссылка, созданная через try_clone, которая не даст его разрушить?
Или внутри try_clone просто пресловутое копирование дескриптора?)

*На самом деле* же никакого сокета нет, есть просто int с номером дескриптора. И этот int можно копировать сколько угодно (поэтому метод try_clone может существовать вообще).

Как в Rust делить ресурсы?

Для этого зайдите в инвентарь или в ваш сундук где хранятся ваши вещи, после этого нажмите на интересующий вас предмет правой кнопкой мыши и перенесите в другую ячейку. Так можно разделить "стак" на два "стака" Если вы хотите еще раз разделить, проведите ту же самую манипуляцию, некоторые ресурсы можно делить так же, только вводя еще и цифры, сколько вам необходимо оставить, а сколько забрать с собой . Некоторые ресурсы не делятся, а хранятся в одном экземпляре.

Работа с кучей в Rust

Продолжаем изучать Rust нетрадиционным способом. В этом раз будем разбираться в нюансах работы с кучей, для их понимания понадобятся: сырые указатели, выделение памяти в куче, размер экземпляра типа, запись значений в кучу и чтение из нее, const и static, unit-like структуры, переопределение глобального аллокатора, печать стека вызовов.

Это, определенно, overkill для одной статьи, а вот половину списка вполне можно освоить.

Сырые указатели (Raw Pointers)

  • Брать адреса можно сколько угодно, а вот разыменование указателя — опасная затея, так что добро пожаловать на территорию unsafe<>
  • Для ряда случаев, например, при нестандартном выравнивании или работой с неинициализированной памятью, надо использовать ptr::addr_of!() / ptr::addr_of_mut!()
  • Документация по методам сырых указателей: primitive.pointer

Выделение и освобождение памяти

  • Пара alloc() / dealloc() указана в The Rustonomicon при разборе RawVec
  • Nomicon то ли отстает, то ли упрощает
  • Как обстоят дела на самом деле с RawVec , рассмотрим позже, для этого нужны неведомые пока (в рамках серии статей) конструкции языка
  • Вызов handle_alloc_error — рекомендованный (". are encouraged to call this function") способ обработки ошибок выделения памяти
  • handle_alloc_error() имеет сигнатуру pub fn handle_alloc_error(layout: Layout) -> ! — "фатальная" функция, из таких не возвращаются

Размер экземпляра типа

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

1. Sized Types. Их размер известен во время компиляции и можно создать экземпляр типа. Несколько примеров, где размер экземпляра больше нуля:

Пора сказать пару слов про кортеж (tuple). Это структура с безымянными полями:

2. Zero Sized Types (ZST). Подмножество Sized, размер экземпляра типа равен нулю, но все еще можно его создать.

К ZST относятся:

  • Пустые структуры
  • Unit-like структуры
  • Пустые кортежи
  • Пустые массивы
. Подавать layout таких типов в функции выделения памяти категорически нельзя

Ну т.е. можно, но результатом будет undefined behavior.

3. Empty Types. Экзотические типы, экземпляров которых не существует.

NeverType (на текущий момент тип не "стабилизирован"):

4. Dynamically Sized Types (DSTs). Размер таких типов неизвестен во время компиляции:

  • интерфейсы (traits);
  • срезы (slices): [T], str.

Rust не примет такую запись:

Интересный вопрос — почему, ведь можно посчитать размер памяти, которая требуется для "Hello there!"? Есть требование, что все экземпляры Sized-типа должны иметь одинаковый размер, вот ему-то значения str и не соответствуют (т.е. единого размера нет), так что — &str и DST.

Далее, если интересно, см.:

Запись / чтение

Теперь у нас все готово для того, чтобы отправлять переменные в Сумрак и выводить обратно.

  • Важно: Деструктор для p при этом НЕ вызывается, т.е. Rust в глубинах вызова как бы "забывает" про эту переменную (текущая последовательность: раз, два).

Для того чтобы посмотреть, когда же вызывается деструктор, реализуем Drop для Point :

Все вместе при запуске дает результат:

Т.е. сначала записываем, затем читаем, и только потом вызывается деструктор у прочитанного значения.

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