Crust of rust - серия стримов для углубления знаний по Rust
После изучения базы по Rust (подробности в гайде) я стал искать, как углубить знания. Попалась неудачная серия материалов, но потом я нашёл то, что нужно — YouTube-канал Jon Gjengset с отличной серией видео Crust of Rust.
С первой лекции про lifetime annotations я понял: это именно тот контент, которого не хватало. Ниже — обзор тем Crust of Rust и заметки того, что зацепило.
Lifetime annotations
Lifetimes — первое, что бросается в глаза в Rust и часто даётся тяжело. В других языках этого почти нет, поэтому приходится перестраивать привычное мышление.
В лекции тема раскрыта хорошо. Самое классное, что автор затрагивает множество полезных подтем. Например, как писать итератор, чем отличаются String
и &str
, как использовать ref mut
, Option::take
, возврат Option?
из функции.
Subtyping и variance
Повторил для себя subtyping и variance. Автор объясняет чётко, но без базовых знаний видео будет тяжело восприниматься. Для закрепления материала он советует Lifetime variance in Rust.
Из интересного:
- для владеющего типа через указатель стоит добавлять
PhantomData
и использоватьNonNull
вместо сырого указателя — такой тип будет инвариантен поT
; - для невладеющего типа нужно задавать variance вручную через
PhantomData
иногда с подвывертом, чтобы указать нужный variance.
Пример:
struct OwningType<T> {
p: NonNull<T>, // *mut T,
_pd: PhantomData<T>
}
struct NotOwningType<T> {
p: *mut T,
_covariant: PhantomData<fn()->T>,
//_contravariant: PhantomData<fn(_:T)->()>,
//_invariant: PhantomData<fn(_:T)->T>,
}
Drop check
При реализации Drop
компилятор предполагает доступ &mut
к полям. Поэтому в таком типе нельзя сделать partial move из структуры. В Nightly Rust есть обход через #[may_dangle]
.
Dynamic dispatch
Не все трейты превращаются в trait object (dyn SomeTrait
). Сигнатура методов трейта кодируется в vtable для dyn
, и некоторые формы методов в таблицу поместить нельзя.
Волшебное ограничение where Self: Sized
может решить несколько проблем:
-
позволяет делать
dyn SomeTrait
, блокируя методы, несовместимые с object safety:- статические методы и ассоциированные функции (без отсылок к
self
); - методы, принимающие или возвращающие
self
по значению; - обобщённые (generic) методы. В этом случае есть обход через динамическую диспетчеризацию;
- статические методы и ассоциированные функции (без отсылок к
-
полностью запрещает object safety, если повесить условие на сам трейт. Редкий кейс, но иногда применяется.
trait SomeTrait {
type SomeInnerTraitType;
fn usual_method(&self);
fn static_method() where Self: Sized;
fn by_value(self) where Self: Sized;
fn ret_by_value(&self) -> Self where Self: Sized;
fn template_method<T: AnotherTrait>(&self, x:&T) where Self: Sized;
fn template_method_dyn(&self, x:&dyn AnotherTrait);
}
fn foo(t: &dyn SomeTrait<SomeInnerTraitType = ConcreteType>);
?Sized
По умолчанию generic-параметры (T
) считаются Sized
. Чтобы принимать unsized-типы, нужно явно писать ?Sized
.
В трейтах наоборот: они определяются как Self: ?Sized
, чтобы компилятор мог автоматически выводить dyn SomeTrait
.
В итоге:
- в generic-функциях надо явно указывать
Sized
или?Sized
; - в трейтах это не требуется.
trait SomeTrait { }
fn foo<T: SomeTrait>(t: &T) { } // fail to call foo(&"hello")
// компилятор разворачивает так:
trait SomeTrait where Self: ?Sized { }
fn foo<T: SomeTrait + Sized>(t: &T) { }
impl SomeTrait for dyn SomeTrait {}
// для unsized
fn foo<T: SomeTrait + ?Sized>(t: &T) { } // ok to call foo(&"hello")
Async/await
Вводная лекция по асинхронному программированию. У автора есть более глубокие разборы:
- The What and How of Futures and async/await in Rust
- Decrusting the tokio crate
- Building an asynchronous ZooKeeper client in Rust
Но все-таки что то да вынес для себя:
- при
select/join
на нескольких future они poll’ятся в текущем task; чтобы реально вынести на другие потоки, нуженspawn
; #[async_trait]
— удобный сахар, но он прячет аллокацию в куче (Box<dyn Future>
); сейчас уже есть поддержка async trait, и если не нуженdyn SomeAsyncTrait
, можно обойтись без этого макроса;- обычные мьютексы можно использовать в async-функциях, если внутри критической секции нет
.await
; иначе нужен async-мьютекс.
#[async_trait]
trait SomeTrait {
async fn foo(&mut self) -> SomeType;
// desugars to:
// fn foo(&mut self) -> Pin<Box<dyn Future<Output = SomeType>>>;
}
trait NotDynTrait {
async fn foo(&mut self) -> SomeType;
// desugars to:
// fn foo(&mut self) -> impl Future<Output = SomeType>;
}
Build scripts / FFI
В других изученных мной материалах про это почти не было, а на работе буквально первые задачи были про использование сторонних библиотек. В Rust-мире знание про биндинги критично: любой серьёзный проект будет использовать внешние библиотеки, а не только крейты на Rust.
Build scripts (build.rs
) — это возможность cargo запускать код до сборки. По сути, аналог CMake-логики в C++. Без них FFI не обойтись: хотя бы для линковки shared-library.
На практике:
- Пишем
build.rs
сprintln!("cargo:rustc-link-lib=dylib=mylib");
. - Для биндингов используем
bindgen
. - Можно пробовать
cbindgen
,cxx
илиautocxx
для упрощения жизни.
Доп. видео из лекции Unsafe & FFI in Rust / Ryan Levick можно пропустить — там примитив. Гораздо полезнее будет посмотреть доклад Jack O’Connor:
- From Rust to C and Back Again — Seattle Rust User Group, April 2025 (пример с
small_vec
) - From Rust to C and Back Again: an introduction to “foreign functions” (пример с
linked_list
).
Заключение
Из дополнительных плейлистов Jon Gjengset я хочу посмотреть:
- Advanced topics in Rust
- Decrusted — разбор крейтов. Хочу глянуть, как минимум,
tokio
иserde
.
Читал мнение, что стримы с кодингом не помогают учиться программированию. Возможно это так, но у меня есть практика на работе, а дома я в спокойном режиме слушаю и подтягиваю теорию.
Crust of Rust — это не просто “стрим с кодом”, скорее похоже на практику в универе, где тщательно разбирается один аспект языка.