Rust box что это
Команда Rust рада сообщить о выпуске новой версии, 1.41.0. Rust — это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение.
Если вы установили предыдущую версию Rust средствами rustup , то для обновления до версии 1.41.0 вам достаточно выполнить следующую команду:
Если у вас ещё не установлен rustup , вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.
От переводчиков
С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов.
Данную статью совместными усилиями перевели andreevlex, blandger, funkill, P0lunin и nlinker.
Возврат типажа с dyn
Компилятору Rust нужно знать сколько места занимает результат каждой функции. Это обозначает, что все ваши функции имеют конкретный тип результата. В отличие от других языком, если у вас есть типаж, например Animal , то вы не можете написать функцию, которая вернёт Animal , по той причине, что разные реализации этого типажа будут занимать разное количество памяти.
Однако есть простой обходной путь. Вместо не посредственного возврата типажа-объекта, наши функции могут возвращать Box , который содержит некоторую реализацию Animal . box - это просто ссылка на какую-то память в куче. Так как размер ссылки известен статически и компилятор может гарантировать, что она указывает на аллоцированную в куче реализацию, мы можем вернуть типаж из нашей функции!
Rust пытается быть предельно явным, когда он выделяет память в куче. Так что если ваша функция возвращает указатель-на-типаж-в-куче, вы должны дописать к возвращаемому типу ключевое слово dyn , например Box<dyn Animal> .
The Rust Programming Language
Участники 1.41.0
Множество людей собрались вместе, чтобы создать Rust 1.41.0. Мы не смогли бы сделать это без всех вас, спасибо!
Rust — сохраняем безразмерные типы в статической памяти
А почему бы просто не взять какой-нибудь linked_list_allocator от Фила, дать ему пару килобайт памяти и воспользоваться обычным Box типом, или даже взять какой-нибудь простейший bump аллокатор, ведь мы хотим использовать его лишь для того, чтобы создать несколько глобальных объектов, но есть множество сценариев, когда куча не используется принципиально? Это и дополнительная зависимость от целого alloc крейта и дополнительные риски, что использование кучи выйдет за рамки строго детерминированных сценариев, что будет приводить к трудноуловимым ошибкам.
С другой стороны, мы можем просто принимать &'static dyn Trait и таким образом переложить заботу о том, как получить такую ссылку, на конечного пользователя, но чтобы обеспечить потом доступ к этой ссылке, нам необходимо использовать примитивы синхронизации или же воспользоваться unsafe кодом, с другой стороны, конечный пользователь тоже должен воспользоваться ими, чтобы создать такую ссылку. В конечном итоге у нас получается или двойная работа или unsafe в публичном API, что довольно плохо. Да и в целом, Box обладает гораздо более широкой областью применения, например, его можно использовать для организации очереди задач в очередном futures executor.
Что же такое безразмерные типы?
Тут очень важно понимать, что для каждого конкретного типа компилятор точно знает внутреннее устройство его безразмерного аналога, проблема в том, что один и тот же безразмерный dyn Display может быть получен из самых разнообразных конкретных типов, чем и обеспечивается динамический полиморфизм. И именно поэтому можно приводить к безразмерным типам лишь ссылки и указатели, уж размер указателя компилятору всегда известен.
Ссылки и указатели в Rust это не всегда просто адреса в памяти, в случае с DST типами, помимо адреса хранится еще и объект с метаданными указателя, но гораздо проще это все осознать, если просто взглянуть на код стандартной библиотеки.
Отсюда видно, что указатель представляет из себя адрес на данные и, в некоторых случаях, какие-то байты с метаданными после. Получается, что размер указателя, в общем случае, может быть вообще любым, вот так, для примера, выглядят метаданные для любого dyn Trait - это просто статическая ссылка на таблицу виртуальных функций.
Таким образом, в текущей реализации, размер &dyn Display на x86_64 составляет 16 байт, а когда мы пишем такой вот код:
Компилятор генерирует объект VTable и сохраняет его где-то в статической памяти, а обычную ссылку заменяет на широкую, содержащую кроме адреса еще и указатель на таблицу виртуальных функций. Ссылка на таблицу виртуальных функций статическая и не зависит от места расположения значения, таким образом, для того, чтобы создать желаемый Box<dyn Display> из искомого значения a , нам необходимо извлечь метаданные из ссылки на dyn_a и все это вместе скопировать в заранее приготовленный для этого буфер. Чтобы все это сделать, нам необходимо использовать nightly features: unsize и ptr_metadata.
Для получения &dyn T из &Value используется специальный маркерный трейт Unsize , который выражает отношение между Sized типом и его безразмерным альтер-эго. То есть, T это Unsize<dyn Trait> в том случае, если T реализует Trait .
Помимо извлечения метаданных, данная функция еще и вычисляет размещение в памяти для метаданных и смещение по которому следует сохранять значение, но об этом мы подробнее еще поговорим.
Обратите внимание, что мы берем Layout от ссылки на метаданные, а не DynMetadata<Dyn>::layout , последний описывает размещение VTable , но нас интересует размещение самого DynMetadata , будьте внимательны!
Пишем свой Box
Вот, теперь у нас есть все необходимое, чтобы написать наш Box , его код довольно простой:
Конструктор, который копирует данные и метаданные в предоставленный буфер, используя довольно удобное API указателя.
А вот и код, который собирает байты назад в &dyn Trait :
Дальше смело можно добавить реализацию Deref и DerefMut , в конце концов, в данном случае это как раз тот самый случай, для которых эти самые типы и были созданы.
Казалось бы, все замечательно, можно использовать библиотеку в боевом коде. Но, постойте, мы же написали unsafe код, как мы вообще можем быть уверены в том, что нигде не нарушили никакие инварианты? К счастью, существует такой проект, как Miri, который интерпретирует промежуточное представление MIR, генерируемое компилятором rustc, используя специальную виртуальную машину. Таким образом, можно находить очень многие ошибки в unsafe коде, подробнее об этом можно почитать в этой статье. Давайте попробуем запустить наши тесты используя Miri.
Ага! Вот и нашлась довольно серьезная проблема, которую мы упустили, и которую нам наша x86 архитектура просто взяла и простила - невыровненный доступ к памяти. Напомню, что процессоры при работе с памятью используют машинные слова, размер которых обычно равен размеру указателя, поэтому компиляторы вставляют в типы, которые не кратны размеру машинного слова, дополнительные байты для выравнивания, тоже самое касается и полей структур. В нашем случае, мы просто подряд пишем байты метаданных и значения в буфер, начиная с какого-то адреса, никак ничего не проверяя, поэтому может возникать ситуация, когда адреса полей становятся не кратными машинному слову.
Чтобы починить проблему логичнее всего рассчитать смещение от начала выданного нам буфера, которое удовлетворит требования к выравниванию, для этого мы будем брать указатель на выданный нам буфер, причем в том случае, когда мы передаем его по значению важно, чтобы он был размещен как поле структуры, в противном случае мы получим указатель на буфер, рассчитаем смещение для него, а позже этот буфер может быть перемещен в другую область памяти и наше рассчитанное смещение перестанет быть верным.
Вот собственно и все, после этого Miri больше не показывает ошибок выравнивания.
Хочу еще сказать несколько слов относительно типа Layout , в нем содержится два поля size , которое содержит размер памяти в байтах, необходимый для размещения объекта, и align - это число (причем всегда степень двойки), которому должен быть кратен указатель на объект данного типа. И таким образом, чтобы починить выравнивание, мы просто вычисляем сколько нам нужно прибавить к адресу начала буфера, чтобы получить адрес кратный align . Дополнительно довольно доступно написано про выравнивание у у Фила.
Заключение
Ура, теперь мы можем писать вот такой вот код!
Итак, мы при помощи unsafe и некоторого количества nightly фич смогли написать тип, позволяющий размещать полиморфные объекты на стеке или в статической памяти без использования кучи, что может быть полезным во многих случаях. Хотя, конечно, каждый раз при получении ссылки на объект приходится дополнительно вычислять адрес метаданных и значения, но мы не можем просто так взять и сохранить эти адреса как поля структуры, в этом случае она станет самоссылающиеся, что довольно неприятно в Rust контексте, это не работает с семантикой перемещения. В целом, если воспользоваться pin API, и сделать нашBox неперемещаемым, то можно будет позволить себе эту оптимизацию, а заодно и обеспечить возможность работать с любыми Future типами.
Хочу еще сказать напоследок, что не стоит бояться писать низкоуровневый unsafe код, но стоит 10 раз подумать над его корректностью и обязательно использовать Miri в CI тестах, он отлавливает довольно много ошибок, а разработка низкоуровневого кода требует очень большой внимательности к деталям всевозможным граничным случаям. В конечном счете, именно знания того, как в реальности реализована та или иная языковая абстракция, позволяет перестать воспринимать её как черную магию. Часто все намного проще и очевиднее, чем кажется, стоит просто копнуть чуть поглубже.
А еще важно иногда выходить за рамки stable Rust, чтобы быть в курсе, куда же язык дальше развивается и тем самым расширять свой кругозор.
Что вошло в стабильную версию 1.41.0
Основными новшествами Rust 1.41.0 являются ослабление ограничений на реализацию типажей, улучшения cargo install , новый формат файла Cargo.lock более дружелюбный для работы с git , и новые гарантии для Box<T> , связанные с FFI. Смотрите подробности выпуска для дополнительной информации.
Ослабление ограничений при реализации типажей
Для предотвращения поломок в экосистеме, когда зависимость добавляет новые реализации типажа, Rust использует правило сироты (orphan rule). Суть в том, что реализация типажа допустима только если типаж или тип, который его реализует, является локальным, т.е. определённым в текущем крейте. Однако это достаточно сложно, когда используются обобщения.
До версии Rust 1.41.0 это правило было слишком строгим, мешая композиции. Например, предположим что ваш пакет реализует структуру BetterVec<T> , и вы хотите иметь возможность конвертации его в Vec<T> из стандартной библиотеки. Вы бы написали следующий код:
… который является примером паттерна:
В версии Rust 1.40.0 этот impl был запрещён правилом сироты, так как From и Vec определены в стандартной библиотеке, которая является чужим крейтом по отношению к текущему крейту. Были способы обойти это ограничение, такие как шаблон newtype, но они часто были громоздкими или даже невозможными в некоторых случаях.
Хотя From и Vec всё ещё остаются чужими, типаж (в данном случае From ) параметризован локальным типом. Поэтому, Rust 1.41.0 позволяет этот impl .
Для более подробной информации читайте отчёт о стабилизации и предложение RFC.
cargo install обновляет пакеты если они устарели
При помощи cargo install вы можете установить в систему исполняемый крейт. Эта команда часто используется для установки популярных CLI-инструментов, созданных комьюнити и написанных на Rust.
Начиная с Rust 1.41.0, cargo install также может обновлять установленные крейты, если с момента установки появился новый релиз. До этого выпуска единственным возможным вариантом было использование флага --force , который позволял переустановить исполняемый крейт даже если он не нуждался в обновлении.
Менее конфликтный формат Cargo.lock
Для обеспечения консистентных сборок, Cargo использует файл с названием Cargo.lock , который содержит версии зависимостей и контрольные суммы. К сожалению, формат организации данных в нём мог привести к ненужным конфликтам слияния при изменении зависимостей в отдельных ветках.
Rust 1.41.0 представляет новый формат для этого файла, разработанный специально для уменьшения конфликтов. Новый формат будет использоваться для всех новых lock -файлов, в то время как существующие файлы будут использовать предыдущий формат. Узнать больше о вариантах, которые привели к новому формату, вы можете в PR, в котором его добавили.
Больше гарантий при использовании Box<T> в FFI
Начиная с Rust 1.41.0, мы заявляем, что Box<T> , когда T: Sized , теперь совместим по ABI с типами указателей в C ( T* ). Таким образом, если вы определяете функцию extern "C" в Rust и вызываете её из C, ваша функция в Rust теперь может использовать Box<T> для какого-то T , и использовать T* в соответствующей функции на C. В качестве примера, на стороне C вы можете иметь:
… а на стороне Rust у вас будет:
Заметим однако, что несмотря на то, что Box<T> и T* имеют одинаковое представление и ABI, Box<T> обязательно должен быть не-null, выровнен и быть готовым для деаллокации глобальным аллокатором. Чтобы обеспечить эти требования, самое лучшее — это использовать такие Box 'ы, которые порождаются глобальным аллокатором.
Важно: По крайней мере в настоящее время вы должны избегать использования типов Box<T> для функций, которые определены в C, но вызываются из Rust. В этих случаях вы должны отразить типы как можно ближе к определению в C. Использование типов наподобие Box<T> , когда определение в C использует просто T* может привести к неопределённому поведению.
Изменения в библиотеке
В версии Rust 1.41.0, мы сделали следующие изменения в стандартной библиотеке:
Подобно Option::map_or и Option::map_or_else , эти методы являются упрощением кода .map(|val| process(val)).unwrap_or(default) .
NonZero* числа теперь реализуют From<NonZero*> если их числовая длина меньше. Например, NonZeroU16 теперь реализует From<NonZeroU8> .
Стабилизированы методы weak_count и strong_count структуры Weak .
Эти методы возвращают количество слабых ( rc::Weak<T> и sync::Weak<T> ) или сильных ( Rc<T> и Arc<T> ) указателей на область памяти.
Сокращение поддержки 32-битной целевой платформы Apple
Rust 1.41.0 будет последним выпуском с текущим уровнем поддержки 32-битных платформ Apple, включая i686-apple-darwin . Начиная с Rust 1.42.0 эти платформы будут понижены до самого низкого уровня поддержки.
Узнать об этом больше вы можете в соответствующей записи в блоге.
Другие изменения
Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения. Мы также приступили к внедрению оптимизаций MIR, которые должны ускорить компиляцию: вы можете узнать о них в блоге "Inside Rust".
Использование Box<T> для ссылки на данные в куче
Наиболее простой умный указатель - это box, чей тип записывается как Box<T> . Такие переменные позволяют хранить данные в куче, а не в стеке. То, что остаётся в стеке, является указателем на данные в куче. Обратитесь к Главе 4, чтобы рассмотреть разницу между стеком и кучей.
У Box нет проблем с производительностью, кроме хранения данных в куче вместо стека. Но он также и не имеет множества дополнительных возможностей. Вы будете использовать его чаще всего в следующих ситуациях:
- Если у вас есть тип, размер которого не может быть известен во время компиляции и вы хотите использовать значение этого типа в контексте, который требует точного размера
- Когда у вас есть большой объем данных и вы хотите передать его во владение, но убедиться, что данные не будут скопированы, когда вы это сделаете
- Когда вы хотите иметь значение и вам важно только то, что это тип, который реализует конкретный типаж, а не является конкретный типом
Мы продемонстрируем первую ситуацию в разделе "Реализация рекурсивных типов с помощью Box" . Во втором случае, передача владения на большой объем данных может занять много времени, потому что данные копируются через стек. Для повышения производительности в этой ситуации, мы можем хранить большое количество данных в куче с помощью Box. Затем только небольшое количество данных указателя копируется в стеке, в то время как данные, на которые он ссылается, остаются в одном месте кучи. Третий случай известен как типаж объект (trait object) и глава 17 посвящает целый раздел "Использование типаж объектов, которые допускают значения разных типов" только этой теме. Итак, то что вы узнаете здесь, вы примените снова в Главе 17!
Использование Box<T> для хранения данных в куче
Прежде чем мы обсудим этот вариант использования Box<T> , мы рассмотрим синтаксис и как взаимодействовать со значениями, хранящимися в Box<T> .
В листинге 15-1 показано, как использовать поле для хранения значения i32 в куче:
Листинг 15-1: Сохранение значения i32 в куче с использованием box
Мы определяем переменную b как имеющую значение типа Box , указывающее на значение 5 в куче. Эта программа напечатает b = 5 ; в этом случае мы можем получить доступ к данным в поле, как если бы это были данные в стеке. Как и любое значение во владении, данная память будет освобождена, когда box выйдет из области действия, что происходит с b в конце main . Освобождается память, занимаемая box (хранится в стеке), и тех данных, на которые он указывает (хранятся в куче).
Размещение единственного значения в куче не очень полезно, поэтому вы не будете часто использовать box сам по себе таким способом. Иметь единственное значение i32 в стеке, где они хранятся по умолчанию, подходит в большинстве ситуаций. Давайте рассмотрим случай, когда Box позволяет определять типы, которые были бы невозможны, если бы у нас не было Box.
Включение рекурсивных типов с помощью Boxes
Во время компиляции Rust должен знать, сколько места занимает тип. Некоторый тип, чей размер не может быть известен во время компиляции, является рекурсивным типом (recursive type), где значение может иметь в своём составе другое значение того же типа. По причине того, что это вложение значений может теоретически продолжаться бесконечно, Rust не знает, сколько пространства памяти необходимо для значений рекурсивного типа. Однако Box имеет известный размер, поэтому используя Box в определении рекурсивного типа, можно его реализовать.
Давайте рассмотрим cons список (cons - функция конструктор, создаёт объекты памяти, которые содержат два значения или указатели на значения), который является распространённым в функциональных языках программирования типом данных, как пример рекурсивного типа. Тип "cons список", который мы определим, является простым, за исключением рекурсии; поэтому концепции, используемые в примере, с которым мы будем работать, будут полезны и в более сложных ситуациях, связанных с рекурсивными типами.
Больше информации о cons списке
cons список (cons list) - это структура данных, которая пришла из языка программирования Lisp и его диалектов. В Lisp, функция cons (сокращение от "construct function" функция-конструктор) создаёт новую пару используя два аргумента, один из которых значение, а другой - пара. Эти пары, содержащие другие пары, образуют список.
Концепция функции конструктора прошла свой путь и превратилась в более общий функциональный, программный жаргон: "to cons х onto у" неформально означает создание нового экземпляра контейнера, помещая элемент х в начале нового контейнера, за которым следует контейнер y.
Каждый элемент в cons списке содержит два элемента: значение текущего элемента и следующий элемент. Последний элемент в списке содержит только значение называемое Nil без следующего элемента. Cons список создаётся путём рекурсивного вызова функции cons . Каноничное имя для обозначения базового случая рекурсии - Nil . Обратите внимание, что это не то же самое, что понятие “null” или “nil” из главы 6, которая является недействительным или отсутствующим значением.
Хотя функциональные языки программирования часто используют cons списки, этот список не является широко используемой структурой данных в Rust. Большую часть времени, когда есть список элементов в Rust, лучше использовать Vec<T> . Более сложные рекурсивные типы данных являются полезными в различных ситуациях, но начиная изучение с cons списка, мы можем исследовать, как box-ы позволяют определить рекурсивный тип данных без особых проблем.
Листинг 15-2 содержит объявление перечисления cons списка. Обратите внимание, что этот код не будет компилироваться, потому что тип List не имеет известного размера, что мы и продемонстрируем.
Листинг 15-2: Первая попытка определить перечисление для представления структуры данных cons списка из значений i32
Примечание: мы реализуем cons список, который для примера содержит только значения i32 . Чтобы определить тип cons список для хранения значений любого типа, мы могли бы использовать обобщённые типы, как обсуждалось в главе 10.
Использование типа List для хранения списка 1, 2, 3 будет выглядеть как код в листинге 15-3:
Листинг 15-3: Использование перечисления List для хранения списка 1, 2, 3
Первое значение Cons содержит 1 и другой List . Это значение List является следующим значением Cons , которое содержит 2 и другой List . Это значение List является ещё один значением Cons , которое содержит 3 и значение List , которое наконец является Nil , не рекурсивным вариантом, сигнализирующим об окончании списка.
Если мы попытаемся скомпилировать код в листинге 15-3, мы получим ошибку, показанную в листинге 15-4:
Листинг 15-3: Ошибка, получаемая при попытке определить бесконечное рекурсивное перечисление
Ошибка сообщает, что этот тип "имеет бесконечный размер". Причина в том, что мы определили List с рекурсивным вариантом: он содержит другое значение самого себя. В результате Rust не может понять, сколько места ему нужно для хранения значения List . Давайте разберёмся, почему мы получаем эту ошибку. Во-первых, давайте посмотрим как Rust решает, сколько места ему нужно для хранения значения нерекурсивного типа.
Вычисление размера нерекурсивного типа
Вспомните перечисление Message определённое в листинге 6-2, когда обсуждали объявление enum в главе 6:
Чтобы определить, сколько памяти выделять под значение Message , Rust проходит каждый из вариантов, чтобы увидеть, какой вариант требует наибольшее количество памяти. Rust видит, что для Message::Quit не требуется места, Message::Move хватает места для хранения двух значений i32 и т.д. Так как будет использоваться только один вариант, то наибольшее пространство, которое потребуется для значения Message , это пространство, которое потребуется для хранения самого большого из вариантов перечисления.
Сравните это с тем, что происходит, когда Rust пытается определить, сколько места необходимо рекурсивному типу, такому как перечисление List в листинге 15-2. Компилятор смотрит на вариант Cons , который содержит значение типа i32 и значение типа List . Следовательно, Cons нужно пространство, равное размеру i32 плюс размер List . Чтобы выяснить, сколько памяти необходимо типу List , компилятор смотрит на варианты, начиная с Cons . Вариант Cons содержит значение типа i32 и значение типа List , и этот процесс продолжается бесконечно, как показано на рисунке 15-1.
Рисунок 15-1: Бесконечный List , состоящий из бесконечных вариантов Cons
Использование Box<T> для получения рекурсивного типа с известным размером
Rust не может понять, сколько места выделить для типов определённых рекурсивно, поэтому компилятор выдаёт ошибку в листинге 15-4. Но ошибка включает в себя это полезное предложение:
В этом предложении "косвенность" означает, что вместо сохранения значения напрямую, мы изменим структуру данных для хранения косвенного значения, с помощью хранения указателя на значение.
Поскольку Box<T> является указателем, Rust всегда знает, сколько памяти нужно для Box<T> : размер указателя не изменяется в зависимости от размера данных, на которые он указывает. Это значит, что мы можем поместить тип Box<T> в вариант перечисления Cons вместо помещения List напрямую. Поле Box<T> будет указывать на следующее значение List , которое будет в куче, а не внутри варианта Cons . Концептуально у нас все ещё есть список, созданный списками, "содержащими" другие списки, но эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга.
Мы можем изменить определение перечисления List в листинге 15-2 и использование List в листинге 15-3 на код из листинга 15-5, который будет компилироваться:
Листинг 15-5: Определение List , которое использует Box , чтобы иметь известный размер
Варианту Cons понадобится размер i32 плюс место для хранения данных указателя Box. Вариант Nil не хранит значений, поэтому ему нужно меньше места, чем варианту Cons . Теперь мы знаем, что любое значение List будет занимать размер i32 плюс размер данных указателя Box. Используя Box, мы сломали бесконечную рекурсивную цепочку, так что компилятор может определить размер, необходимый для хранения значения List . На рисунке 15-2 показано как теперь выглядит вариант Cons .
Рисунок 15-2: List , размер которого не безграничен, потому что Cons содержит Box
Box-ы обеспечивают только косвенность и выделение в куче; у них нет других специальных возможностей, таких как те, которые мы увидим у других типов умных указателей. Они также не имеют накладных расходов из-за этих специальных возможностей, поэтому могут быть полезны в случаях, похожих на cons список, где косвенность - единственная нужная функциональность. Мы рассмотрим ещё больше вариантов использования типа Box в главе 17.
Тип Box<T> является умным указателем, потому что он реализует типаж Deref , что позволяет значениям Box<T> обрабатываться как ссылки. Когда значение Box<T> выходит из области видимости, то данные кучи, на которые указывает Box, очищаются благодаря реализации типажа Drop . Давайте рассмотрим эти два типажа более подробно. Эти два типажа будут ещё более важными для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в оставшихся частях этой главы.
Читайте также: