@RoadToGamedev

Rust unsafe, какие есть подводные камни и как подходить к дизайну C API?

Этот пост я написал с целью найти ответы на свои вопросы.
Они связаны с Rust и его unsafe привязками к C.
Чем я дальше иду, тем мне больнее смотреть на что то.
Может от моей глупости, может от чего-то другого.
Моя цель построить небольшой биндинг, части Raylib.

Первое что отмечу. Мне не подходит инструмент bindgen.
Мне не нравится 2 тысячи строк констант (цифра из одной особо мне любопытной привязке к Raylib).
Из которых 1900 никогда не будут мной использоваться.
Может я что-то не понимаю и он как то их не вшивает?
Но так же и с функциями / структурами.
Еще раз напомню что я, пытаюсь сделать биндинг малой части Raylib.
У меня есть опыт разработки привязок на Python и Golang с нуля.
Так же я использовал эту привязку на C, C++, D (немного).
Есть много причин почему я работаю с этой библиотекой.
От лицензии, до личных интересов и опыта.

В один прекрасный момент я решил подумать о Rust, как о замене Go.
Мне нравится делать игры, для себя, для друзей, частично для бизнеса.
Смотря на Rust, мне так и хочется на нем программировать, но...
Unsafe мне тыкает что бы я бежал.
Наверное не только в Rust, но я с этим встретился только в Rust.

Возьму простой пример на C:
RLAPI void DrawRectangle(int posX, int posY, int width, int height, Color color);
Rust:
extern "C" {
fn DrawRectangle(pos_x: c_int, pos_y: c_int, width: c_int, height: c_int, color: Color);
}
Все хорошо, за одной поправкой. Если я хоть что-то изменю. Rust меня только похвалит.
В Color я могу указать &Color и отправить туда указатель. Так работает много где.
Результаты.. Самые разные. От неправильного цвета который установил указатель (видимо указатель стал цветом), до рабочего кода. Указывая ссылку на структуру Sound.
Я получал рабочий результат. Как будто все хорошо. И это меня немного добивает.
Ладно, это мелочи. Нам нужно быть чуточку внимательнее к самому блоку вызова Си.
Хотя как я могу указать ссылку на структуру, но не отправить структуру и это где то работает, а где то нет?
Ну вот как? Ладно поехали дальше...

Когда мы все правильно экспортировали. Я начал задумываться, а что значит безопасное API к unsafe.
Прочитав мануалы от Rust и пару книг. Посмотрев чужой код. Я совершенно потерял ощущение реальности.
В одной книге упор был на то, что начальная функция должна контролировать дочерние.
А точнее мы должны сделать так, что бы язык нам сам как бы говорил. Ты не можешь использовать функцию Б,
пока не вызовешь А. Звучит логично, на практике.. Как то так.

Это псевдокод, но он +- отражает реальность.
// привычное api на всех языках.
fn main() {
window_init(1152,648,"Hello, world!");
set_target_fps(60);
while !window_should_close() {
begin_drawing();
clear_background(&Color{r:0,g:0,b:0,a:255});
draw_texture(&testr,10,10,&Color{r:255,g:255,b:255,a:255});
end_drawing();
}
close_window();
}

// На Rust пытаются сделать так.
fn main() {
window_init(1152,648,"Hello, world!");
set_target_fps(60);
while !window_should_close() {
begin_drawing();
clear_background(&Color{r:0,g:0,b:0,a:255});
draw_texture(&testr,10,10,&Color{r:255,g:255,b:255,a:255});
}
}


Короче код, функция закрытия окна и окончание рисование закончится само по себе, при выходе из зоны видимости.
Как и Rust, библиотека пытается нас спасти от глупой ошибки! Мне надо бы, сказать ей спасибо.
Примерно так пытаются сделать много где. Пальцами тыкать не буду. Правда все это делается на структурах, потоках и.т.д.
И код выглядит не меньше, а больше.
Сематика с моей точки зрения рушится, придя из мира SDL, можно немного и запаниковать.
Привязка к SDL сделана более разумно чем к другим вещам, наверно. Я сильно не вникал именно в нее.
Так как это не моя цель.
Но вот от других мне становилось все хуже. Я привык что не знаешь как писать код, смотри, анализирую, разбирайся в чужом.
Тут я не уверен что так делать стоит. Но разберемся что не так со всеми этими попытками обезопасить закрытие функции.
Хотя в Go это делается через defer сразу после открытия. Тут пытаются явно автоматизировать.
И что мы получаем? в 90% случаев мне надо явно после конца рисования, произвести какую то логику.
После явного закрытия окна так же. Сохранить логи, запустить или выгрузить что то.
Так как это делают, в попытку повторить Drop за выход из зоны видимости. Портит не только читаемость кода.
Но и много чего еще.. Я не умею шутить, но представьте синтаксис HTML и перестаньте вставлять закрывающий тег.
Выглядеть будет ужасно.

Сейчас я пойду по разным библиотекам, но многие из них схожи в одном плане.
Проверяем на ошибку, но сразу паникуем!

Пример из мира Raylib.
window_init(1152,648,"Hello, world!");
bool = is_window_ready()

//далее логика.

Но во многих фреймворках, я вижу зашитое слово паники.
Как по мне это недопустимо! Мы должны что то сделать в попытках.
А. Устранить проблему, если это возможно.
Б. Уведомить пользователя. Консоли нет. Так что native окошко ошибки.
С. Записать в лог или куда то отправить.

Сложно уместить это в метод, как пытаются сделать многие. Можно конечно, просить анонимную функцию или еще как то..
Но вы понимаете к чему я все? Иногда простые вещи, не стоит пытаться автоматизировать в сторону. Лишь бы не ошибиться.
Но увы я много где вижу просто слово паники или ошибки. Загрузка медиа, управление окном, звук и.т.д.
Не везде конечно, но где вижу. Это провал!

Посмотрим на пример из биндинга к Raylib.
extern crate raylib;
use raylib::prelude::*;
use structopt::StructOpt;

mod options;

fn main() {
let opt = options::Opt::from_args();
let (mut rl, thread) = opt.open_window("Logo");
let (w, h) = (opt.width, opt.height);
let rust_orange = Color::new(222, 165, 132, 255);
let ray_white = Color::new(255, 255, 255, 255);

rl.set_target_fps(60);
while !rl.window_should_close() {
// Detect window close button or ESC key
let mut d = rl.begin_drawing(&thread);
d.clear_background(ray_white);
d.draw_rectangle(w / 2 - 128, h / 2 - 128, 256, 256, rust_orange);
d.draw_rectangle(w / 2 - 112, h / 2 - 112, 224, 224, ray_white);
d.draw_text("rust", w / 2 - 69, h / 2 + 18, 50, rust_orange);
d.draw_text("raylib", w / 2 - 44, h / 2 + 48, 50, rust_orange);
}
}

Вы наверное видите рабочий, хороший код.
Я вижу МАГИЮ!
Первое это магия с потоком и открытием окна. Мы можем посмотреть в документацию и что то понять.
Но становится все более не понятно. Зачем?
Другие две магии лежат в том что окно закрывать и рисование не надо.
Ладно понять как это работает можно. Мне не дано понять зачем?
У нас куча модулей, которые явно не связанны друг с другом и мы имеем кучу разных структур для управления всем этим делом. С какой то стороны я соглашусь, это придает безопасности коду. Но я не могу, его читать.
Что такое d которое дает нам begin_drawing. Это уже не самодокументриемый код. Это набор магий.
Или увеличение кол-во переменных.
Честно, зная меньше Raylib я бы подумал. Вау они используют многопоточный рендеринг.
Или что то в таком духе. На деле это далеко не так...
Но мне очень нравится пример с задержкой времени отрисовки кадра. Кажется с theard sleep в SDL.
Просто смотрю на это и у меня появляется улыбка. Ай да изобретатели, ай да смекалочка.
В общем нормальное, читаемое API превращается в кашу.

Эх потом я посмотрел на исходный код одной игры.. где было 3к строк в одном файле.
50 циклов. По циклу на сцену. И подумал, какие молодцы. Я видимо на столько плохой программист.
Что мне тяжко осилить этот пример.

Какой толк от автоматической выгрузки ассетов.
Если 90% выгружаются загружаются в процессе? Я не понимаю.

Но в итоге выходит я дурак.
Не могу осилить Rust unsafe, потому что не понимаю. А нам правда нужны эти потоки?
Нам правда нужно везде иметь автоматическую выгрузку всего и вся?
Я не понимаю где кончается 1 перегиб в сторону C и заканчивается другой перегиб в сторону Rust.
И вроде язык не плохой и многое можно делать интересно и безопасно, но где кончается этот перегиб?

Мой план такой. Обернуть все вызовы к C простыми функциями. Сделать пару проверок и не канапатить мозги.
Или может есть веские причины делать что то реально такое. . сложное. С потоками, каналами, разными фишками?

Вы мне скажите, Пожалуйста! А то я правда не понимаю.
Нельзя построить менеджер ассетов? Который будет по команде что то чистить или не чистить? Например на хэш карте?

Нельзя делать код который ТОЛЬКО может выполняться в одном потоке. Просто в 1 функции. А то что многопоточное отдельно?
Мне правда нужно 100 и 1 модуль на каждый чих? Я понимаю ООП. Окно объект, Картинки объект.
Но можно явно как то закрывать окно? А картинки оставить рядом с текстурами. Да текстуры например нельзя загружать без открытия окна. И да тут можно ошибиться. Но это точно стоит усложнения логики всего приложения?
Когда все это можно инициализировать после открытия, например даже 1 функцией? Которая за это отвечает.

Есть правда еще один вопрос. Это копирование структур. Выходит так что мы копируем структуру на каждый чих.
Пытаясь явно отправить в Си. В цикле 60 кадров в секунду. Потому что передаем по указателю во врап функцию.
А там уже копируем и отдаем Си. Как то даже не задумывался и не знаю как это протестировать в Rust.
У структур с Copy нет Drop. Точнее он пустой. Вот видите Rust, мне правда слишком ломает голову.
Помогите, пожалуйста! Это так всегда будет или это потом пройдет когда я его пойму? и пойму ли?
Тяжело быть соло разработчиком, а тебе понравилось что то..., а оно как бы само тебя отталкивает.
Увы мне нравится Rust, но кроме как делать игры на нем. Мне больше нечего делать.
А он меня своим unsafe и предложениями в каждой строчке. Будьте осторожны, не трогай unsafe.
Мы за unsafe не отвечаем и.т.д. Сам отталкивает. Эх иногда тяжко без стандарта. Четкого и понятного.
Так что еще раз прошу, помогите кто чем может. Я в плане совета, опыта, может ссылок на не тривиальную литературу.
Какие есть реальные подводные камни? Можно в ЛС.
Словарь Рус. языка не поможет. Сразу говорю. Правда, можете отправить меня обратно в мир Go / C / C++, но я вам это припомню:)
  • Вопрос задан
  • 277 просмотров
Пригласить эксперта
Ответы на вопрос 1
vabka
@vabka
Токсичный шарпист
Не пудри себе мозги и возьми уже готовую безопасную обёртку над raylib
https://crates.io/crates/raylib

А гайд по работе с unsafe - это rustonomicon

unsafe сам по себе просто позволяет использовать сырые указатели + вызывать другие unsafe функции.

Безопасная обётка - это когда ты при помощи типов и всяких валидаций гарантируешь корректное использование.
Вот пример из того что выше:
use raylib::prelude::*;

fn main() {
    let (mut rl, thread) = raylib::init()
        .size(640, 480)
        .title("Hello, World")
        .build();

    while !rl.window_should_close() {
        let mut d = rl.begin_drawing(&thread);

        d.clear_background(Color::WHITE);
        d.draw_text("Hello, world!", 12, 12, 20, Color::BLACK);
    }
}

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

На будущее: не пишите огромную портянку текста с кучей вопросов, а пишите только то что непосредственно относится к основному вопросу.
Другие вопросы задавайте отдельно.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы