Я наоборот вижу в нем смысл именно для МК, RTOS или драйверов ОС.
1) Библиотеки. Ничего уровня embassy или defmt в С нет и не предвидится. В С++ кстати тоже. 2) Безопасность. Нет множества UB, которые в С приходится проверять санитайзерами, хотя это просто не должно было скомпилироваться. 3) Быстродействие. Ассемблер на выходе такой же, а местами и чище чем в С, т.к. у компилятора больше возможностей для оптимизации. Ну и памяти есть меньше, см. например null pointer optimization, который вообще не возможен в С.
Распространенность вообще ортогональна сырости. Есть куча примеров популярных сырых технологий и непопулярных отточенных. Другие аргументы про "сырой язык" будут?
Что касается HAL. Вы же не будете спорить, что STM32 HAL на каждый чих делает кучу действий, которые в нормальном коде занимают 1-2 такта?
А как насчет поддержки всех режимов работы периферии? Как в нем выключить только Noise Error, не выключая Framing Error в UART? Как написать раельно работающий SPI Slave, или еще хуже I2C Slave? Каждый раз приходится после функций HAL менять вручную биты в регистрах.
Сделать кастомное USB (не CDC или HID) устройство вообще не возможно. Проще полностью выкинуть код USB из HAL, и написать свою библиотеку USB.
Можете ли вы аргументировать про "сырой язык"? Например на микроконтроллерах STM32 библиотеки на Rust значительно менее сырые, чем официальный HAL на С.
Вот в этом и беда, что в С++ есть куча разных HAL, и нет ничего универсального. Взгляните для сравнения на embedded-hal для Rust. Если у меня проект на STM32, и вдруг понадобился WiFi, то я меняю 10 строчек кода, и 2 строки в системе сборки (Cargo.toml), и компилирую под ESP32. В С++ мне нужно переписать весь код работы с периферией.
Что мешало это сделать на С++ за ***цать лет? Да вроде ничего, язык позволяет. Но этого нет, и вероятно уже не будет. Потому что поезд уже ушел...
Целочисленное знаковое переполнение - это настоящее UB, а не implementation defined, как вы пытаетесь представить. И компилятор с ним может творить интересные вещи, например сделать конечный цикл бесконечным. Не смотря на то, что процессор 2's complement.
Я потому и написал "до концептов", что они меняют дело. Но минусующие читают по диагонали..
Да, динамическая диспетчеризация = рантайм диспатч, я думал это понятно из названия.
Кстати, если компилятор заинлайнит функцию test из примера, то он разворачивает динамическую диспетчеризацию в статическую, и дальше может инлайнить input. К сожалению, нет никаких гарантий, что он так сделает.
Опять же, возможность использовать утиный подход там, где он очевидно выгоден (с опциональными ограничениями через SFINAE или концепты), куда лучше, чем принципиальная невозможность его использовать.
Утиный подход сильно увеличивает время компиляции (думаю очевидно почему), выдает огромные простыни текста при ошибках (по тем же причинам), и приводит к интересным багам.
Так что у него есть свои плюсы и минусы, просто минусы редко обсуждают.
С другой стороны в С++ не вылетит ошибка компиляции, пока не произойдет инстанцирование шаблона.
Это неудобно пользователям библиотеки: из заголовка функции/шаблона не понятно, что в нее можно передавать.
Это особенно не удобно разработчикам библиотек: приходится писать тесты компиляции на все возможные случаи, и всё-равно иногда не получается покрыть все варианты.
Эту проблему пытались решить через SFINAE (инопланетная технология), и сейчас пытаются решить через концепты. Это говорит о том, что возможно в С++ (до концептов) и есть наихудший вариант шаблонов/дженериков.
А ваш пример вполне себе работает через динамическую диспетчеризацию (dyn Trait), просто её нужно указать явно. Если не указывать, то происходит мономорфизация до типов usize и f32, а два типа не равны друг другу по определению.
Я понял, что неправильно прочитал начальный пример. Извините. Давайте вернемся к началу:
Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.
Если указатель передается в каллбек (FFI), то UB не будет, т.к. ссылка выйдет из области видимости. Если же использовать указатель сразу, то будет UB в очень ограниченном числе случаев (см. исходный RFC для &raw):
невыровненные данные. Но сейчас это уже не скомпилируется, писал выше.
неинициализированная память. Используйте MayBeUninit, для типичного случая unsafe не нужен.
необитаемые и пустые типы. Тут конечно UB, но зачем на них брать указатель??
То есть Miri, по вашему утверждению, объявляет UB то, что с точки зрения абстрактной машины Rust и оптимизаций компилятора ей не является (и не из-за того, что в спорном случае предпочитает выдать ошибку, а из-за того, что у него просто другая семантика)? Вот тут, честно говоря, хотелось бы подтверждение - по идее, если бы это было так, он был бы де-факто бесполезен, поскольку банально выдавал бы слишком много шума.
Правила алиасинга в языке есть, их нужно соблюдать. Компилятор их проверить не может (не всегда может) для указателей, и полагается на программиста с помощью unsafe.
Miri пытается проверять правила алиасинга. Для этого переписывает код после rustc, это не "статический анализатор". Неактивные указатели это терминология Miri, и её нет в языке и компиляторе. Причем там разная терминология для Stacked Borrows и Tree Borrows.
Повторюсь, я изначально всё это писал в предположении, что мы unsafe уже по какой-то причине используем.
Потому что это не получается сделать в Safe Rust, например из-за FFI. Но конкретно случай не выровненных данных не релевантен. Потому что из FFI можно получить указатель, превратить его в ссылку и безопасно читать не выровненные поля без unsafe.
А можно сразу ссылку получить, если C API в другом языке написан правильно.
Вы сами написали именно так, и я разобрал эту конкретную ситуацию:
Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.
И проблема возникнет только если заключать в unsafe большие куски кода, чтобы включить в них еще и разыменование, которое (как вы сами указали) где-то есть.
Правилами алиасинга, вестимо. По крайней мере, вот на такой код Miri ругается:
Miri это не компилятор rustc. В компиляторе и языке нет никаких неактивных указателей, и компилятор не проверяет правила алиасинга для указателей. Так что неактивные указатели это концепция только Miri, а мы до того обсуждали язык и компилятор.
Открывал, почему же. Для ссылок это - ошибка компиляции. Для указателей - нет, а делать по ним ptr.read или *ptr вместо ptr.read_unaligned - это всё так же UB.
Вы рассматриваете какой то сферический Unsafe Rust в вакууме. Если Rust дает возможность работать безопасно с не выровненными полями без unsafe, то зачем же я буду усложнять себе жизнь?
И так по любой концепции: есть безопасный вариант, а есть опасный через unsafe. Причем безопасным пользоваться намного легче. В С нет этого выбора, там всегда будет возможен UB.
что в "unsafe rust" больше шансов поймать UB чем в Си
Если загнать под unsafe весь код, и писать только на указателях, вероятно так и будет. Но зачем, когда для этого есть С?
Да, я в курсе, я упомянул об этом в первом комментарии. И много вы видели ситуаций со случайно добавленным restrict? А случайно создать ссылку, ломающую работу с указателями, и не понять этого, пока не получишь замечание от Miri, - первый попавшийся пример - https://github.com/rust-lang/rust/issues/128803.
Видел, и достаточно. restrict был добавлен не случайно, но при рефакторинге его гарантии случайно поломал другой программист.
Кстати, UB можно вызвать и без restrict, через type punning. Который используется в любой библиотеке сериализации и форматирования на С. И не возможен в Rust. Как и сотни других UB, которые вы не замечаете.
Я про создание из самого T. Грубо говоря, сделать &mut foo as *mut _ вместо &raw mut foo.
Ну то что нужен unsafe вы совершенно случайно забыли:
unsafe { &mut foo as *mut _ } против &raw mut foo Случайно это сделать не выйдет, как бы вы ни пытались.
Использовать ptr, который при создании borrow был помечен как неактивный.
Интересно, что это за неактивный указатель? И кем он помечен?
Мы же говорим про unsafe Rust, верно? Указатель на невыровненное поле тоже должен использоваться правильно (через read_unaligned), иначе это UB.
Очевидно что ссылку вы не открывали. Иначе бы убедились, что в Rust в этом случае ошибка компиляции. Исправить ее можно через read_unaligned, или другими способами.
Я это всё не к тому, что в Rust что-то не так (я сам его очень люблю). Просто то, что в нём есть свои способы выстрелить в ногу неопределённым поведением, которых не было в языках-предшественниках, и попытка писать на unsafe Rust как на "ещё одном Си", скорее всего, в один из них упрётся, - факт, который, AFAIK, признают и разработчики языка в том числе.
Вызвать UB можно и в Rust. Но вы не понимаете главной разницы между ними.
В С и С++ самый короткий и простой синтаксис вызывает UB. Чтобы написать безопасный код, приходится использовать vsnprinf вместо prinft, и так по всей стандартной библиотеке.
Это сознательное решение разработчиков языка и стандарта. Всё в угоду быстродействию. И новые стандарты не исправляют почти ничего, т.к. обратная совместимость.
В Rust небезопасные функции требуют unsafe, у них длинные названия (get_unchecked против get) и неудобный синтаксис (unsafe { &mut foo as *mut _ }) и есть способы вообще запретить unsafe в крейте: #![forbid(unsafe_code)]. Всё чтобы не вызвать UB случайно.
которых не было в языках-предшественниках
То что вы описали, есть и в С++ и С. Пройдите наконец по ссылкам и прочитайте по Pointer Provenance. В С это доступно через restrict.
В Rust случайно создать &mut T из *mut T никак не выйдет. Для этого надо написать конструкцию let ref2 = unsafe {&mut *ptr};, и она специально сделана так странно и неудобно.А вот второй *mut T создается просто копированием: let ptr2 = ptr;. Кроме того для UB нужно создать два&mut T!
потому что некоторые UB из Си в Rust не переехали (знаковое переполнение, например)
Из C в Rust не переехало порядка 99% UB самого языка, и около 90% UB стандартной библиотеки. И я как раз могу это доказать. Для этого достаточно открыть ubbook, и читать в нем статью за статьей, а потом пытаться перенести на Rust. Существенная часть примеров UB в нем работает и в С, и в С++, а другие с минимальными усилиями переносятся на С и тоже вызывают UB.
Выпускать 1.0 нужно когда вы пришли (в первый раз) к стабильному API, и не собираетесь его ломать продолжительное время. Причем тут hotfix вообще?
Например упомянутому в статье крейту base64 уже 9 лет. Я, как и автор статьи, тоже без понятия, чего там можно так долго "мариновать".
Я тоже без понятия. Но я знаю что в мире это широко практикуется не только в Rust. И принимаю мир как он есть.
А вот автор сделал акцент именно на Rust, "смотрите как всё плохо". В совокупности с остальными пунктами (притянуто за уши, местами открытое вранье), и тэгом Юмор, я делаю вывод что это наброс. И об этом ниже еще пара человек написали.
Я думаю что это больше ментальная проблема у разработчиков.
Как быть с разработчиками в Go и Javascript? Это реальность во многих библиотеках/языках.
Например целый язык программирования Zig сознательно остается на версии 0.x у потому что его разработчики еще не считают, что у них есть стабильное API. Их право. Возможно их версия 1.0 будет гораздо лучше текущей, но мы не знаем когда она выйдет.
Godbolt нельзя использовать для сравнения скорости компиляторов и кода. Результат может зависеть от того, лежит ли gcc и rustc в кэше, может они вообще в разных контейнерах крутятся, или на разных серверах.
Используйте специальные инструменты для проведения бенчмарка. Или на худой конец запустите 100 000 раз на компе с прогревом в 1000 запусков, и усредните.
P.S. Похоже что 348 и 1535 это время работы компиляторов, а не программы.
Как правило считается, что между версиями 0.х может быть сломано API, а 0.х.у нельзя ломать. ZeroVer применяется для библиотек, которые разрабатываются недавно и еще не пришли к какому-то API. Обычно через N лет они все-таки выпускают версию 1, и приходят к обычному SemVer. Но некоторые могут зависнуть в 0.x надолго, т.к. не хотят брать на себя обязательства.
В Rust это популярно из-за статической линковки. Можно конечно делать .dll или .so, но этим крайне редко пользуются. В Go аналогично.
Согласен. Против этого может помочь разделение на микросервисы. Идеальный пример - Robot Operating System. IPC с разделяемой памятью, там где это важно. Модули пишутся на С, С++, Rust, Python, C# и еще куче языков. Работает в системах с ограниченными ресурсами: миникомпьютеры и даже микроконтроллеры.
Конечно не везде это возможно, но ROS это как иногда достижимый идеал в коде.
Там всё началось с того что для некоторого API на С не было документации совсем. И в исходном коде тоже. И чтобы с ней интегрироваться, разработчики на Rust попросили сделать документацию, а те ответили в духе "есть же .h, по ним делайте". Но если заменить Rust на любой другой язык, да хоть С++, вылезла бы та же проблема.
будто сишники испугались, что их хотят просто заменить на кого-то помоложе и попродуктивнее, и они просто таким образом защищаются
Я наоборот вижу в нем смысл именно для МК, RTOS или драйверов ОС.
1) Библиотеки. Ничего уровня embassy или defmt в С нет и не предвидится. В С++ кстати тоже.
2) Безопасность. Нет множества UB, которые в С приходится проверять санитайзерами, хотя это просто не должно было скомпилироваться.
3) Быстродействие. Ассемблер на выходе такой же, а местами и чище чем в С, т.к. у компилятора больше возможностей для оптимизации.
Ну и памяти есть меньше, см. например null pointer optimization, который вообще не возможен в С.
Распространенность вообще ортогональна сырости. Есть куча примеров популярных сырых технологий и непопулярных отточенных. Другие аргументы про "сырой язык" будут?
Что касается HAL. Вы же не будете спорить, что STM32 HAL на каждый чих делает кучу действий, которые в нормальном коде занимают 1-2 такта?
А как насчет поддержки всех режимов работы периферии? Как в нем выключить только Noise Error, не выключая Framing Error в UART? Как написать раельно работающий SPI Slave, или еще хуже I2C Slave? Каждый раз приходится после функций HAL менять вручную биты в регистрах.
Сделать кастомное USB (не CDC или HID) устройство вообще не возможно. Проще полностью выкинуть код USB из HAL, и написать свою библиотеку USB.
Можете ли вы аргументировать про "сырой язык"?
Например на микроконтроллерах STM32 библиотеки на Rust значительно менее сырые, чем официальный HAL на С.
В продакт коде однократные вычисления при старте программы можно не считать. Так что я бы сделал Lazy static переменную. А этот подход конечно треш.
Вот в этом и беда, что в С++ есть куча разных HAL, и нет ничего универсального. Взгляните для сравнения на embedded-hal для Rust. Если у меня проект на STM32, и вдруг понадобился WiFi, то я меняю 10 строчек кода, и 2 строки в системе сборки (Cargo.toml), и компилирую под ESP32. В С++ мне нужно переписать весь код работы с периферией.
Что мешало это сделать на С++ за ***цать лет? Да вроде ничего, язык позволяет.
Но этого нет, и вероятно уже не будет. Потому что поезд уже ушел...
Ок, тот же самый пример: конечный цикл бесконечным c -O1 и -O2.
UB это UB при любом уровне оптимизация, иногда даже при -O0.
Тогда зачем же вы используете это UB? Может надежнее сделать ассемблерную вставку?
Целочисленное знаковое переполнение - это настоящее UB, а не implementation defined, как вы пытаетесь представить. И компилятор с ним может творить интересные вещи, например сделать конечный цикл бесконечным. Не смотря на то, что процессор 2's complement.
Я потому и написал "до концептов", что они меняют дело. Но минусующие читают по диагонали..
Да, динамическая диспетчеризация = рантайм диспатч, я думал это понятно из названия.
Кстати, если компилятор заинлайнит функцию test из примера, то он разворачивает динамическую диспетчеризацию в статическую, и дальше может инлайнить input. К сожалению, нет никаких гарантий, что он так сделает.
Утиный подход сильно увеличивает время компиляции (думаю очевидно почему), выдает огромные простыни текста при ошибках (по тем же причинам), и приводит к интересным багам.
Так что у него есть свои плюсы и минусы, просто минусы редко обсуждают.
С другой стороны в С++ не вылетит ошибка компиляции, пока не произойдет инстанцирование шаблона.
Это неудобно пользователям библиотеки: из заголовка функции/шаблона не понятно, что в нее можно передавать.
Это особенно не удобно разработчикам библиотек: приходится писать тесты компиляции на все возможные случаи, и всё-равно иногда не получается покрыть все варианты.
Эту проблему пытались решить через SFINAE (инопланетная технология), и сейчас пытаются решить через концепты. Это говорит о том, что возможно в С++ (до концептов) и есть наихудший вариант шаблонов/дженериков.
А ваш пример вполне себе работает через динамическую диспетчеризацию (dyn Trait), просто её нужно указать явно. Если не указывать, то происходит мономорфизация до типов usize и f32, а два типа не равны друг другу по определению.
Я понял, что неправильно прочитал начальный пример. Извините.
Давайте вернемся к началу:
Если указатель передается в каллбек (FFI), то UB не будет, т.к. ссылка выйдет из области видимости. Если же использовать указатель сразу, то будет UB в очень ограниченном числе случаев (см. исходный RFC для &raw):
невыровненные данные. Но сейчас это уже не скомпилируется, писал выше.
неинициализированная память. Используйте MayBeUninit, для типичного случая unsafe не нужен.
необитаемые и пустые типы. Тут конечно UB, но зачем на них брать указатель??
Правила алиасинга в языке есть, их нужно соблюдать. Компилятор их проверить не может (не всегда может) для указателей, и полагается на программиста с помощью unsafe.
Miri пытается проверять правила алиасинга. Для этого переписывает код после rustc, это не "статический анализатор". Неактивные указатели это терминология Miri, и её нет в языке и компиляторе. Причем там разная терминология для Stacked Borrows и Tree Borrows.
Потому что это не получается сделать в Safe Rust, например из-за FFI. Но конкретно случай не выровненных данных не релевантен. Потому что из FFI можно получить указатель, превратить его в ссылку и безопасно читать не выровненные поля без unsafe.
А можно сразу ссылку получить, если C API в другом языке написан правильно.
Вы сами написали именно так, и я разобрал эту конкретную ситуацию:
И проблема возникнет только если заключать в unsafe большие куски кода, чтобы включить в них еще и разыменование, которое (как вы сами указали) где-то есть.
Miri это не компилятор rustc. В компиляторе и языке нет никаких неактивных указателей, и компилятор не проверяет правила алиасинга для указателей. Так что неактивные указатели это концепция только Miri, а мы до того обсуждали язык и компилятор.
Вы рассматриваете какой то сферический Unsafe Rust в вакууме. Если Rust дает возможность работать безопасно с не выровненными полями без unsafe, то зачем же я буду усложнять себе жизнь?
И так по любой концепции: есть безопасный вариант, а есть опасный через unsafe. Причем безопасным пользоваться намного легче. В С нет этого выбора, там всегда будет возможен UB.
Если загнать под unsafe весь код, и писать только на указателях, вероятно так и будет. Но зачем, когда для этого есть С?
Видел, и достаточно. restrict был добавлен не случайно, но при рефакторинге его гарантии случайно поломал другой программист.
Кстати, UB можно вызвать и без restrict, через type punning. Который используется в любой библиотеке сериализации и форматирования на С. И не возможен в Rust. Как и сотни других UB, которые вы не замечаете.
Ну то что нужен unsafe вы совершенно случайно забыли:
unsafe { &mut foo as *mut _ }
против&raw mut foo
Случайно это сделать не выйдет, как бы вы ни пытались.
Интересно, что это за неактивный указатель? И кем он помечен?
Очевидно что ссылку вы не открывали. Иначе бы убедились, что в Rust в этом случае ошибка компиляции. Исправить ее можно через read_unaligned, или другими способами.
Вызвать UB можно и в Rust. Но вы не понимаете главной разницы между ними.
В С и С++ самый короткий и простой синтаксис вызывает UB. Чтобы написать безопасный код, приходится использовать
vsnprinf
вместоprinft
, и так по всей стандартной библиотеке.Это сознательное решение разработчиков языка и стандарта. Всё в угоду быстродействию. И новые стандарты не исправляют почти ничего, т.к. обратная совместимость.
В Rust небезопасные функции требуют
unsafe
, у них длинные названия(
get_unchecked
противget
)и неудобный синтаксис (
unsafe { &mut foo as *mut _ }
)и есть способы вообще запретить unsafe в крейте:
#![forbid(unsafe_code)]
.Всё чтобы не вызвать UB случайно.
То что вы описали, есть и в С++ и С. Пройдите наконец по ссылкам и прочитайте по Pointer Provenance. В С это доступно через restrict.
В Rust случайно создать
&mut T из *mut T
никак не выйдет. Для этого надо написать конструкциюlet ref2 = unsafe {&mut *ptr};,
и она специально сделана так странно и неудобно.А вот второй *mut T
создается просто копированием:let ptr2 = ptr;
. Кроме того для UB нужно создать два&mut T!
Из C в Rust не переехало порядка 99% UB самого языка, и около 90% UB стандартной библиотеки. И я как раз могу это доказать. Для этого достаточно открыть ubbook, и читать в нем статью за статьей, а потом пытаться перенести на Rust. Существенная часть примеров UB в нем работает и в С, и в С++, а другие с минимальными усилиями переносятся на С и тоже вызывают UB.
Как насчет невыровненных ссылок? Или дивного правила integer promotion, про которое не знает ни один программист, которого я лично спрашивал.
Или pointer provenance, который ломает работающий ранее С код, даже в режиме c99. Да, в каком году его добавили в стандарт? Или еще не добавили?
А может напомнить вам про слабую типизацию, и как результат неявное приведение всего ко всему. Ну и самое типичное - выходы за пределы массива.
Эта музыка будет вечной, и список можно продолжать очень долго.
Выпускать 1.0 нужно когда вы пришли (в первый раз) к стабильному API, и не собираетесь его ломать продолжительное время. Причем тут hotfix вообще?
Я тоже без понятия. Но я знаю что в мире это широко практикуется не только в Rust. И принимаю мир как он есть.
А вот автор сделал акцент именно на Rust, "смотрите как всё плохо". В совокупности с остальными пунктами (притянуто за уши, местами открытое вранье), и тэгом Юмор, я делаю вывод что это наброс. И об этом ниже еще пара человек написали.
Я написал то же самое, но другими словами.
Как быть с разработчиками в Go и Javascript? Это реальность во многих библиотеках/языках.
Например целый язык программирования Zig сознательно остается на версии 0.x у потому что его разработчики еще не считают, что у них есть стабильное API. Их право. Возможно их версия 1.0 будет гораздо лучше текущей, но мы не знаем когда она выйдет.
Godbolt нельзя использовать для сравнения скорости компиляторов и кода. Результат может зависеть от того, лежит ли gcc и rustc в кэше, может они вообще в разных контейнерах крутятся, или на разных серверах.
Используйте специальные инструменты для проведения бенчмарка. Или на худой конец запустите 100 000 раз на компе с прогревом в 1000 запусков, и усредните.
P.S. Похоже что 348 и 1535 это время работы компиляторов, а не программы.
Как правило считается, что между версиями 0.х может быть сломано API, а 0.х.у нельзя ломать. ZeroVer применяется для библиотек, которые разрабатываются недавно и еще не пришли к какому-то API. Обычно через N лет они все-таки выпускают версию 1, и приходят к обычному SemVer. Но некоторые могут зависнуть в 0.x надолго, т.к. не хотят брать на себя обязательства.
В Rust это популярно из-за статической линковки. Можно конечно делать .dll или .so, но этим крайне редко пользуются. В Go аналогично.
Согласен. Против этого может помочь разделение на микросервисы. Идеальный пример - Robot Operating System. IPC с разделяемой памятью, там где это важно. Модули пишутся на С, С++, Rust, Python, C# и еще куче языков. Работает в системах с ограниченными ресурсами: миникомпьютеры и даже микроконтроллеры.
Конечно не везде это возможно, но ROS это как иногда достижимый идеал в коде.
Там всё началось с того что для некоторого API на С не было документации совсем. И в исходном коде тоже. И чтобы с ней интегрироваться, разработчики на Rust попросили сделать документацию, а те ответили в духе "есть же .h, по ним делайте". Но если заменить Rust на любой другой язык, да хоть С++, вылезла бы та же проблема.
Согласен.