мне не нравится реальность
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
# Как 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
На морозе телефон разряжается так быстро, что я не успеваю включить яндекс карты
Ну и насыщенный сегодня день
Я очень плохой человек если наываю модули с разницей в одну букву?