Как стать автором
Обновить

Комментарии 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.

НЛО прилетело и опубликовало эту надпись здесь

В 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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории