Комментарии 21
Проблема в том, что id объекта в подобной библиотеке — аналог небезопасного указателя.
Безопасная обёртка должна скрывать его, а не основывать на основе его получения все операции с API.
С одной стороны — хотелось бы, да. Хотелось бы хранить всё состояние внутри структур rust, и полагаться на встроенные механизмы безопасности. В своём проекте я даже некоторые функции, которые гарантированно выполняются в один вызов так и делаю — фетчу всё то состояние, которое мне необходимо, на его основе создаю свою структуру и работаю с ней.
С другой стороны — я пока не нашёл лучшего способа инкапсуляции. С сырыми указателями плохо то, что они просто используются. Они не кричат о том, что их надо проверить на Null, их время жизни ограничено временем жизни переменной указателя и т.п.
Собственно, в этом отношении, работа с «указателями» в этом примере становится не более опасной, чем вызов элемента из вектора по индексу. Я добавляю как минимум гарантии:
того, что, мы сами не поломаем свои же объекты, потому что теперь можно менять один объект за раз
того, что объект умрёт вместе с «родителем» и я не смогу им пользоваться позже.
того, что объект из FFI слоя потребует себя проверить на Null (Option).
Может быть, GUI — не самая лучшая аналогия, потому что редко бывают ситуации, когда у GUI несколько владельцев. И тут можно было бы попробовать хранить копию состояния в структуре, оставляя лишь те проверки, которые отвечают за вход изначальных данных. И общаться с высокоуровневой обёрткой в стиле imgui\egui. Мне просто хотелось что-то более близкое и понятное, чем то, с чем общаюсь сейчас сам.
Но оригинальная среда, из которой я выцепил этот пример — это DAW, где есть треки, на них есть items, у них есть takes, в них есть source, в котором лежат сырые данные. И я физически не могу контролировать то, что происходит в проекте. Также, как мне кажется расточительным тратить на каждую операцию дополнительные O(n) на итерацию, особенно, если оно ещё будет в каждом вызове общаться с FFI: это и так несколько накладно, а про свой API я точно знаю, что можно нарваться на O(n²) за один обход, допустим, миди-событий в треке.
Ну и в принципе, с одной стороны, хотелось проработать пример достаточно для того, чтобы он «крякал», когда monkey_ffi
выполняет какую-то операцию. И мог «упасть». А с другой стороны, это пример контроля параметризации мутабельности. Поэтому, чем больше «обвеса» — тем сложнее выцепить основной механизм, который тестирую, и про который рассказываю.
Когда вы обращаетесь к вектору по индексу — вектор гарантирует, что будет либо паника, либо он вернёт корректный элемент.
Когда вы передаёте id в стороннюю библиотеку на другом языке — библиотека может без всякой проверки взять неинициализированный элемент из своего внутреннего массива, и обратиться по указателю оттуда.
Нет, когда я обращаюсь к вектору по индексу Vec::get()
, он предлагает мне обработать Option
. То же самое делает моя обёртка. Здесь механизм «проверки на валидность» лежит в самом модуле monkey_ffi
, который возвращает None
, если я прошу объект с id выше, чем у него. И, да, там даже unsafe код есть)
При работе с указателем у нас есть несколько других механизмов. Самый «лобовой» — ptr::is_null()
. У меня, допустим, API предоставляет способ валидации указателя: отправляешь ему указатель, а он говорит, можно ли им пользоваться.
Я в этом случае сделал обёртку над обёрткой. Я получаю сырой указатель по методу get()
, который внутри проверяет, живой ли указатель и паникует по необходимости. Если хочется проверить без паники — есть метод, который делает то же самое, но возвращает Result
.
А, поскольку, эта операция тоже не бесплатная — есть способ выключить эту проверку на время. Допустим, в начале функции валидировать указатель, потом выключить проверки и работать на механизмах rust.
Вот тут можно посмотреть, как оно выглядит в проекте.
Здесь механизм «проверки на валидность» лежит в самом модуле monkey_ffi, который возвращает None, если я прошу объект с id выше, чем у него.
Это совершенно неинтересный случай, который никак не подходит под ваше вступление:
Но вот когда нужно написать семантически-безопасный API над функциями и данными, которые вообще не безопасны
Почему библиотеку, которая проверяет любые переданные ей на вход параметры и никогда не падает, вы называете "вообще не безопасной"?
Я не понимаю, почему мы обсуждаем одну проблему, когда статья про другую?
И потрудитесь прочитать, пожалуйста: она не «никогда не падает» и «проверяет любые переданные на вход параметры». А предоставляет такой механизм.
Ещё раз: проблема в том, чтобы на этапе компиляции (cargo check
) знать, что ты не удалишь трек №2 во время того, как редактируешь что-то на треке №3. Я пытаюсь семантически связать те объекты, которые не связаны в оригинальном API.
тут в TWIR 468 после появления GATов один чувак решил в miri сделать что-то похожее.
В Rust Зависимые типы (на данный момент) — это конкретные типы, которыми может оперировать trait (интерфейс). В общем, это способ замкнуть реализацию трейта на конкретном наборе структур. А зависимые они потому, что, в последствие, достаточно указать один параметр для трейта, чтобы инициализировать остальной набор типов.
Здесь в примере, завтипы не раскрылись в полной мере, потому что для этого надо было бы добавить ещё 1-2 уровня потомков. Но присутствуют:
trait Button<T: ProbablyMutable>
where
Self: Sized,
{
type Parent; // Зависимый тип
}
А потом мы его имплементируем для конкретных типов:
impl<'a, T: ProbablyMutable> Button<T> for WindowButton<'a, T> {
type Parent = &'a Window<'a, T>;
fn new(parent: Self::Parent, id: usize) -> Option<Self>;
}
impl<'a> ButtonMut for WindowButton<'a, Mutable> {
type Parent = Window<'a, Mutable>;
}
В принципе, если у нас появится целая пачка типов, которые нужны трейту — можно также их выводить из одного дженерик-параметра:
impl<'a, T: ProbablyMutable> Button<T> for WindowButton<'a, T> {
type Root = &'a Root<'a, T>;
type Parent = &'a Window<'a, T>;
type Child = WindowButtonText<T>;
fn new(parent: Self::Parent, id: usize) -> Option<Self>;
fn get_text(&self) -> Self::Child;
}
Это называется не "зависимые типы", а "ассоциированные типы".
Хм. Пожалуй.
Я, когда для себя переводил «assotiated functions» и «assotiated types» решил, что, раз ассоциированные функции всё равно в 90% случаев легче называть методами (а в остальных 10%, хоть и не очень корректно — статическими методами).
То и assotiated types, скорее всего всё-таки завтипы. Т.к. ассоциированные типы на русском я впервые читаю в вашем комментарии.
Раз уж вы здесь — расскажите, пожалуйста, чем завтипы отличаются от ассоциированных?
Считайте завтип дженериком, параметризованным значением, известным только в рантайме.
Переименовал тред. Хотя, теперь мне меньше понятно, когда нужны зависимые типы. Особенно, если хочется побольше уверенности на этапе компиляции.
Вот для этой уверенности они и нужны. Там фокус в том, что хотя точный тип известен только в рантайме — проверка типов всё равно выполняется на этапе компиляции. Но сложность в том, что для их использования компилятору надо научиться доказывать теоремы, иначе нормально пользоваться ими будет невозможно.
Простейший пример с которого можно начинать понимать как они могут быть использованы — проверка границ массивов. Если в Rust есть два режима работы — с проверкой при каждом обращении (которую, может быть, уберёт оптимизатор если звёзды сойдутся правильно) и небезопасный, то завтипы позволяют описать тип "число от 0 до N-1, где N — длина массива" и безопасно избежать почти всех проверок.
Поимею небольшую наглость вкинуть собственную статью около темы - https://habr.com/ru/post/477330/ (реализация на Rust и расписана в целом достаточно подробно, так что проблем с пониманием быть вроде бы не должно).
Небольшой вопрос. Зачем использовать PhantomData<T>
если T
итак тип нулевого размера и не является ничем кроме маркера?
Он нужен, как минимум, в первый раз, чтобы ввести параметризацию по этому параметру. Т.к. у нас нет никакого реального объекта, который принимал бы для конструирования ProbablyMutable
— rust будет ругаться, что мы требуем обозначения этого параметра для структуры.
Потом PhatnomData тоже может пригодиться, если захочется более гибко обходиться с мутабельностью детей. Но я пока что заметил, что потом API получается лучше без дополнительных сущностей.
Зато столкнулся с ситуацией, когда пришлось брать на вход два иммутабельных объекта, и внутри для создания мутабельного коннектора между ними, кастовать родителя через mem::transmute
. Грязноватый, небезопасный хак. Но, т.к. эти «пины» не мог изменять, никто, кроме коннектора — я решил, что это достаточно безопасно.
Не уверен что понял первый аргумент. Rust так и так попросит явно указать тип PhantomData<T>
при создании структуры (иначе не скомпилируется).
Я хотел сказать что вместо PhantomData<Immutable>
или PhantomData<Mutable>
можно было сразу писать Immutable или Mutable соответственно.
А. Может быть. Я не подумал, что можно просто аргумент в new() положить в структуру.
Всё-таки, иногда phantom data нужен: если есть ограничение по трейту, а объекта нет.
Нужно прописать аннотацию вида
struct Str<T: ProbablyMutable, P: Parent<T>>{
parent: P
}
Вот так не скомпилируется. Для этого приходится добавлять PhantomData
Rust: параметризуем мутабельность через маркеры и ассоциированные типы