мне не нравится реальность
504 subscribers
1.33K photos
57 videos
56 files
1.02K links
Мне не нравится реальность
N.B. waffle is unhinged

- кормить назад: @meowaffle
- кормить вперёд: github.com/sponsors/WaffleLapkin
- чят: https://xn--r1a.website/+5Dtuan4dVE5kYTcy
- блог: blog.ihatereality.space
Download Telegram
Ощущаю себя немного «13-летней девочкой» (впрочем это лучше чем ощущать себя бесчувственным комком грусти)
здравствуйте
Настроение написать пост про программирование 🤔
— Доктор, меня все игнорируют
— Следующий

Вот так я себя зачастую чувствую. В моём PR мои доводы за это изменения игнорируют и мейнтейнер спорит с другим чуваком...
2 коммита по делу, 6 коммитов фиксов на пару строк
# Как Unsafe мне в ногу стрелял

Во многих языках программирования, которые предоставляют какие-то гарантии безопасности [0^], есть инструменты чтобы эту самую безопасность сломать. Например рефлексия с C#/Kotlin/etc, или unsafe в Rust. О последнем я сегодня и хочу поговорить.

unsafe в расте даёт много возможностей (на самом деле список достаточно короткий, но мощный), но его корректность не может проверить компилятор. Поэтому о корректности алгоритма используещего unsafe приходится думать програмисту.

Вот простая диограмма показывающая когда вам нужно использовать unsafe:
Хочешь написать unsafe?
|
|
Нет <--+--> Да
| |
| |
---->+<----
|
|
перехочешь

Дело в том что с unsafe одна ошибка и ты ошибся. Простая опечатка может привести к совершенно катастрофическим ошибкам вроде утечек памяти, UB [1^], сегфолтов и т.д..
Ещё у unsafe есть свойство протекать. Ошибка допущенная в одном месте в unsafe {} блоке может выстрелить в совершенно другом месте, из-за чего отладка некоректно написанного unsafe-кода становится той ещё головной болью.

Теперь, когда я рассказал что такое «unsafe» и с чем его едят, перейдём к моему случаю из жизни. Я писал функцию которая должна была инициализировать массив используя функцию, переданную пользователем библиотеки.

Те кто знают что такое unsafe уже напряглись, потому что вызывать в unsafe коде пользовательскую функцию надо с огромной осторожностью. Какие-бы (безопасные) непотребства пользовательская функция не выполняла, твой unsafe код не должен привести к UB и подобным ошибкам.

Но в итоге через пол часа дум, гуглежа, копипасты и ругани матом я написал эту функцию. Получилось примерно 100 строк запутанного unsafe кода в корректности которого я не был до конца уверен. Но функция работала как и было положенно, можно ликовать!

...или нет...
Unsafe может выстрелить совершенно неожиданно. Так и произошло в этом случае. Из 9 тестов этой функции, которые я написал, 2 не прошли. Смысл был в том, что если пользовательская функция паникует или возвращает ошибку, то деструкторы значений которые уже инициализированны, не вызывались. Это могло бы привести к утечкам памяти [2^].

"Но как так?? Я же это предусмотрел, я вызываю деструкторы! Это самая сложная часть кода..." — моя первая мысль. Признаюсь, в начале я думал что неправильно написал тесты (считать дропы и ловить паники это не самые очевидные вещи). Но в тестах ошибок не было. Я пытался сделать mre [3^] этого бага, но ничего не выходило.

Я потратил ещё около 2-х часов (в 4 раза дольше чем я писал функцию!) чтобы понять в чём была моя ошибка, и ещё минут 10 чтобы её починить. А в итоге ошибка была в том, что я забыл вызвать одну функцию, из-за чего я дропал не T, а MaybeUninit<T> [4^] что является no-op. В обычной ситуации компилятор раста ударил бы меня по рукам, но в unsafe коде он бессилен.

Мораль:
0. не пишите unsafe код
1. серьёзно, не пишите
2. не пишите и .
3. хорошо тестируйте unsafe код с разным и неправильным вводом, если вы всё-таки его написали (это то место, где типы не спасают [5^])
4. будьте внимательны и осторожны при выходе из вагонов написании unsafe кода

#вафля_программист
[0^]: под безопасностью имеется в виду безопасность памяти, публичность/приватность методов и т.д.
[1^]: UB — Undefined behavior, это код который может привести к чему угодно. Код с UB может просто ничего не делать, вызвать функцию которая в коде ни разу не вызывается, призвать демонов, напечатать 42 в консоль или отключить тормоза в твоей машине. Что угодно. Насколько я это понимаю, UB нужно чтобы компилятор мог проводить более агрессивные оптимизации.
[2^]: Не вызвать деструктор (aka Drop::drop) в расте считается безопасным поведением. Так что ничего фундаментально плохого я не сделал. Но всё же такое поведение неприемлимо.
[4^]: MaybeUninit<_> — специальный тип в расте для работы с неинициализированной памятью. Так как значение этого типа могут быть неинициализированными, то для них деструкторы не вызываются.
P.S. вот сам код с ошибкой. тут видно (тот случаай когда подскаки типов рулят), что создаётся слайс из MaybeUninit<Item>, который затем дропается (что является no-op из-за MaybeUninit).

Закоментированный метод превращает &mut [MaybeUninit<Item>] в &mut [Item] что решает проблему невызванных деструкторов.
Писать людям на английском так страшно... не хочу ошибиться и случайно нагрубить...
Вызов методов в расте би лайк:

let a: A = ...;

a.method();
A::method(&a);
<A as Trait>::method(&a);
<_>::method(&a);

// nightly version
A::method.call((&a,));
<A as Trait>::method.call((&a,));
<_>::method.call((&a,));
A::method.call_mut((&a,));
<A as Trait>::method.call_mut((&a,));
<_>::method.call_mut((&a,));
A::method.call_once((&a,));
<A as Trait>::method.call_once((&a,));
<_>::method.call_once((&a,));


(да это всё делает одно и тоже и я не щучу)
Вспомнил ещё три способа:

Trait::method(&a);
<_ as Trait>::method(&a);
<A>::method(&a);

Так-же &Антон напомнил что call{_once,_mut,} это тоже функции так что...

<_>::call_once(<_>::method, (&a,));

Немного сумаществия: (playground)
Как же неприятно »_«
Статическая типизация сожгла мне гречку.
Forwarded from 小猫は勇者である
Чуть не опоздал на электричку, ааа
This media is not supported in your browser
VIEW IN TELEGRAM