Pull to refresh
12
0
Иван Дубров @idubrov

Разработчик

Send message

Да, я в полном восторге от Rust на микроконтроллерах (STM32). Я от C++ уйти хотел ещё даже когда про Rust слыхом не слыхивал; C++ -- это всё-таки не тот язык на котором я бы хотел хобби-проект делать. Старый он, и кривой просто ужасно.

В Rust https://rtic.rs/ понравилось (тогда оно ещё RTFM называлась). Вообще, идея языка где компилятор будет находить ошибки типа гонок состояний, в том числе и на микроконтроллерах, -- очень интересно. Даже если это и будет работать только для менее "требовательных" к производительности случаев.

Но я так, сварщик не настоящий (на Rust три года коммерческого кода писал, но без микроконтроллеров -- REST сервисы).

Профессиональная разработка для микроконтроллеров -- там, наверное, уже по-другому. Rust сырой, библиотек или нет или постоянно меняются, сертификации нет, опыта в индустрии -- тоже. Может быть, вот эти ребята https://oxide.computer/ что-то поменяют -- по-моему, они для микроконтроллеров тоже что-то делают.

>Почему-то мне тоже казалось, что можно биты с 1 на 0 по одному менять, но в моей реализации я вообще всего два состояния использую: активная страница и пустая.

А, вот почему. Они в своей реализации сами используют это:

https://documentation.help/AN2594-EEPROM/eeprom_8h-source.html#l00057

Состояние страницы переходит из 0xffff (пустая) => 0xeeee (копируем на эту страницу) => 0x0000 (активная).

Так что попробуйте одно и то же слово несколько раз программировать (но важно что новое значение может только сбрасывать биты 1 в 0, но не наоборот), может, получится 😀 (P.S. упс, я вижу, что вы написали, что уже пробовали...).

P.P.S. Хм, если буквально читать, что они пишут, то 0x0000 они всегда разрешают записать? Может, три состояния всё-таки можно (0xffff => что-нибудь отличное от 0x0000 => 0x0000)?

В апноуте выше, насколько я помню, предлагается дописывать на страницу. Идея в том, чтобы писать новые данные на ту же страницу, а при чтении брать последнюю "версию".

Я по этому апноуту делал реализацию на Rust (https://github.com/idubrov/eeprom). Я правда, с потерей питания особо не заморачивался -- при записи, если кончилось место, была вероятность потерять "новые" данные пока мы копируем страницу.

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

В ситуации, когда у нас две активных страницы я просто выбирал любую. Наверное, ход мыслей был такой, что в такой ситуации их содержимое должно совпадать. Что не совсем верно -- если питание пропало в момент "спасения", возможна ситуация двух активных страниц, причём мы ещё и можем начать дописывать в одну из них (и тогда они будут отличаться). Наверное, можно было починить тем, что при инициализации всегда затирать остальные "активные" страницы. А, ну и при копировании я не проверяю, что новая страница на самом деле пустая (может оказаться, что копировать начали, а в середине всё сломалось). Но по идее, это можно проверить просто чтением всей страницы, чтоб убедиться, что она вся пустая.

В нашей системе обработка ошибок на основе failure и собственных гм… инструментов мимикрировала в что-то очень похожее на исключения. Цепочки «исключений», даункасты, стектрейсы :D

Я Any::type_id очень рад… хе-хе. Впрочем, мы по уши на ночной сборке, поэтому более-менее пофиг.

А что с объектами? Чем пугают?
но, надеюсь, в реальные проекты оно все-таки не пойдет.

Ничё-ничё, у меня уже продакшн версия на подходе. Надо статью написать (наверное, на английском в этот раз).

Так правильно. Вот ты, как инженер, как часто открываешь сайт Rust-а? Сайт просто не для тех, кто уже пишет на Rust.

Цель была не использовать обёртки. Прелесть конверсии &mut Data -> &mut Object в том, что с заимствованием хорошо стыкуется.


С Rc изменяемости не получится (&mut self).


Плюс, Rc — заразны. В нашем случае API у Object более сложный, и весь API пришлось бы перетряхивать, чтобы возвращать Rc (или Box).

Это правильный вопрос, но тут много переменных чтобы так просто ответить. Если по-честному, то мы вот это вот не используем. Это был так, эксперимент, чтобы джокера к рукав засунуть, на случай чего.


На языке с динамической типизацией были бы другие проблемы.


Энтерпрайз (особенно, когда всё делается практически с чистого листа, как у нас :D) — это как водяной матрас. Сложность никуда не девается, её можно только в разные части передавливать.

Да, я знаю, как проще. К сожалению, полностью описать проблему у не могу (во-первых, это будет на пару постов объяснений, а во-вторых, это раскроет некоторые детали того, над чем мы работаем которые я рассказывать не должен). Вкратце, можно представить задачу примерно таким тестом:


#[test]
fn test() {
  use serde_json::Value;
  let x: Value = Value::String("hello".into());
  let y: Value = Value::String("hello".into());

  let x_obj: &dyn Object = adapt(&x, "id");
  let y_obj: &dyn Object = adapt(&y, "string");

  assert_eq!(x_obj.type_name(), "id");
  assert_eq!(y_obj.type_name(), "string");
}

Наверное, я немного запутал тем, что использовал термины "система типов" и "имя типа" (fn type_name). Это просто некоторая абстрактная мета-информация, которую я хочу приписать к данным. Идея здесь в том, что эта информация не всегда доступна во время компиляции.


Я немного тут расписал: https://habr.com/post/432202/#comment_19459492


P.S. Про 99% — совершенно верно. Я думаю, те, кому этот приём может пригодиться и сами могут его реализовать. А те, кто не может — им это и не нужно.

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


Почему это плохо? Бывает код, который неявно полагается на то, что если "сущность" одна и та же, то и ссылка должна быть та же.


  1. Например, если код делает synchronized(adapted) (но синхронизироваться на левых объектах — плохая идея).
  2. Сравнивает по ссылке. Например, какой-то внутренний кеш. Мы обходим граф объектов и проверяем, видели мы какой-то объект или нет. Если каждый узел графа будет обёрткой, то можем оказаться в ситуации, когда мы объект видим второй раз, а ссылка на него другая (так как это обёртка)
  3. Просто с точки зрения памяти, хотя это не сильно принципиально. С коллекциями беда — их то ли копировать (память), то ли заворачивать (морока).

В случае Rust, эта самая идентичность ещё более критична если речь идёт о изменяемом заимствовании (&mut). Правда, тут получается, что как бы сами "создали" себе проблему (выбором Rust).


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

Этот TraitObject — то же самое по сути, но требует включение нужной фичи (raw). Мой вариант будет работать и на стабильном Rust. Понятно, что если просто убрать этикетку (#![feature(raw)]), сущности это не поменяет и код будет ровно такой же хрупкий :D


Не понял второй части. Можно через обёртки, да, собственно варианты #1 и #2 про это и были, но с ними, например, изменяемый интерфейс уже сложнее сделать (т.к &mut self может быть только один).


Можно, но если дальше обобщать (например то, что сама "система типов" — это типаж с функцией fn adapt<'a>(&'a self, data: &'a Data, meta: &MetaInfo) -> Self::Adaptor<'a>, то уже GAT-ы нужны.


На верхнем уровне, задача такая:


  1. Взять, например, serde_json::Value. Взять JSON схему (с диска прям прочитать). Скрестить данные и схему и заставить &Value вести себя как "типизированный объект", &dyn Object (т.е отвечать на вопросы "какие у тебя есть поля?", и.т.д.).
  2. Взять структуры. Поступить аналогично (этот вариант тривиален — просто реализуем типажи для структуры).
  3. Обобщённый код (например, валидация) может работать с обоими ^^ через обобщённый интерфейс типаж-объекта Object).

Другой вариант — это всегда держать пару (&Data, &Meta) и при обходе данных обновлять параллельно. Т.е let (child, child_meta) = (data.get_field("field"), meta.get_field("field")), но там тоже свои проблемы.

Information

Rating
Does not participate
Location
California, США
Registered
Activity