Как стать автором
Обновить
2947.3
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

Прокачиваем силу — Rust и Windows API

Время на прочтение10 мин
Количество просмотров15K


Недавно я написал статью Трясём стариной — или как вспомнить Ассемблер, если ты его учил 20 лет назад. В статье рассказывается о том, как изучать ассемблер на примере игрушки 2048. Возможно для целей самой статьи игрушка была подходящая, но конечный результат меня немного удручил. Бинарник размером в 10 килобайт, который потребляет 2 мегабайта памяти, из-за неправильно слинкованной библиотеки резал глаза.

Посему я задался вопросом, а как это можно было бы сделать правильнее? Наверняка есть намного более удачное решение. (И организовал ещё один конкурс с призами в конце статьи)

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

Причём это не значит, что вы будете использовать какую-то нестандартную библиотеку. Встречайте — windows-rs, проект поддерживаемый Microsoft. Ваш билет в мир Windows, если вы пишете на Rust.

При написании этой статьи я понял, что мы берём понемногу из двух миров — Windows API и rust. Определённые моменты будут казаться очевидными для rust разработчиков, другие моменты будут казаться очевидными для Windows разработчиков. Так как это — статья для всех, то я решил что будет лучше, если объясню больше, чем меньше.

▍ Введение


Для тех кто незнаком с Rust — вам необходимо будет подтянуть свои знания. Хотя бы потому что с момента своего создания в 2010 году язык завоёвывает популярность. С 2016 года язык появлялся в отчётах stackoverflow в списках самых любимых языков. В 2021 году Rust остаётся самым любимым языком на stackoverflow, что не мешает ему оставаться очень нишевым языком, доступным только избранным. В основном потому, что сам по себе Rust это низкоуровневый язык, который хоть и красив и приятен, но всё же учится не за 20 минут по видео из ютуба.

Rust используется в ядре Linux. Естественно, так как Rust был изначально создан в Mozilla Foundation, то большое количество компонентов Firefox написано именно на нём. В Microsoft решили, что не стоит отставать, и начали использовать Rust в части своих проектов.
И если год назад вы могли увидеть в репозитории упоминания, что проект не находится в стабильном состоянии и не рекомендуется к использованию в работе, то сейчас эти упоминания из проекта пропали. (Хотя, для того чтобы вы могли воспользоваться результатами этого проекта, вам всё равно придётся установить Rust Nightly билд, и вы успеете насмотреться на предупреждения от компилятора).
Итак, в чём заключается проект? Все Windows API импортированы в Rust и вы можете использовать любые стандартные функции прямо из вашей программы, без большого количества колдовства.

▍ Начало работы


Итак, что же нам нужно сделать, для того чтобы начать работать с Windows API в Rust.

Для начала, нам потребуется rust nightly.

rustup default nightly
rustup update

После этого вам нужно будет создать новый проект на rust и сразу создать дополнительную библиотеку под названием bindings.
cargo new testproject
cd testproject
cargo new --lib bindings
.
И, после этого добавить в cargo.toml в нашем проекте зависимости для подключения библиотеки Microsoft.

Cargo.toml

[dependencies]
bindings = { path = "bindings" }
windows = "0.21.1"

На момент написания статьи, актуальная версия библиотеки 0.21.1, посему везде будем использовать именно эту версию.

После этого в самой папке с библиотекой bindings нам нужно будет добавить в Cargo.toml следующий текст:
[package]
name = "bindings"
version = "0.1.0"
edition = "2018"

[dependencies]
windows = "0.21.1"

[build-dependencies]
windows = "0.21.1"

Итак, что у нас тут получается? У нас есть проект под названием testproject, в нём есть библиотека bindings. Цель этой библиотеки — подключать зависимости для того, чтобы вы могли работать с Windows API в вашем приложении.

Сам файл bindings/src/lib.rs будет состоять из одной команды:

::windows::include_bindings!();

Это — вызов макроса, который подключит все необходимые зависимости.

И теперь, самое интересное, файл bindings/build.rs

fn main() {
    windows::build! {
        Windows::Win32::UI::WindowsAndMessaging::MessageBoxA,    
    };
}

Естественно, по старой доброй традиции мы начнём с вывода бесполезного сообщения на экран. Посему мы начнём с подключения стандартного класса MessageBox, который позволит вывести на экран сообщение.

▍ Написание программы


Итак, мы закончили с подготовкой. Что делать теперь? Теперь всё просто. Здесь можно брать документацию Windows API и искать то, что нам интересно.

Для начала, давайте разберёмся с очень простым действием вывода сообщения «Привет Хабр». Это всё очень просто, но позволит нам посмотреть на два основных отличия winrs программы от обычного растовского бинарника.

Первое — функция main теперь должна возвращать windows::Result<()>. В данном случае мы будем возвращать пустой кортеж, но так как мы находимся в Windows, мы можем вернуть очень много вариантов значений. Для тех, кому это может понадобиться, Result принимает error, подробности здесь.

fn main() -> windows::Result<()> {
    Ok(())
}

Второе — все вызовы самих Windows API должны производиться через директиву unsafe.

Я видел множество каких-то непонятный священных войн по поводу того, что использование директивы unsafe должно быть запрещено законом. Если честно, я никогда не понимал из-за чего происходят подобные войны. Давайте посмотрим на официальную документацию. Здесь нам ясно и чётко рассказывают, что в использовании unsafe нет ничего дьявольского, и оно разрешено Женевской конвенцией. Вам просто необходимо знать, что да как использовать.

Ок. Лезем дальше. Давайте откроем документацию Microsoft и найдём в ней MessageBoxA.

У нас есть четыре параметра, которые нам надо передать этой функции.

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

  • hWnd — дескриптор основного окна, в котором будет показано уведомление. Так как MessageBoxA — это модальное окно, то выполнение команд в основном окне заблокируется, пока будет активно уведомление. Так как у нас нет никакого окна, то сюда можно смело передавать NULL.
  • lpText/lpCaption — указатель типа Long на строку, строки, которые будут показывать в заголовке и теле окна.
  • uType — набор констант, которые зададут поведение, тип кнопок и иконок в этом MessageBox

Надо заметить, что Microsoft хорошо потрудились с созданием правильных биндингов. Данную функцию в rust мы будем вызывать как

MessageBoxA(None, "Привет", "Это игра 2048 для Хабры", MB_OK | MB_ICONINFORMATION );

Как вы видите, нам не пришлось изобретать велосипед, и пытаться создавать тип NULL, которого не существует в Rust, да и передача строк не требует шести преобразований их в разные форматы. Поэтому нам не нужно извращаться и писать что-то типа:

MessageBoxA(
            std::ptr::null_mut(),
            lp_text.as_ptr(),
            lp_caption.as_ptr(),
            MB_OK | MB_ICONINFORMATION
        );
    }

Всё достаточно цивильно.

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

use bindings::Windows::Win32::UI::WindowsAndMessaging::{MessageBoxA, MB_OK, MB_ICONINFORMATION};


fn main() -> windows::Result<()> {
    unsafe {
        MessageBoxA(None, "Привет", "Это игра 2048 для Хабры", MB_OK | MB_ICONINFORMATION );
    }
    Ok(())
}

Смотрим: Потрачено!



Замечательно! Повсюду кракозябры, как в 1999 году. Что делать? Разбираемся и понимаем, что MessageBoxA работает c ANSI строками, в то время как MessageBoxW работает с Unicode (Wide strings). Собственно говоря, эта нотация сохранилась во всех Windows API. Я не знаю причин не использовать W версии функций. Но, будьте аккуратны, большое количество писателей мануалов на английском языке не понимают разницы, и вы увидите загруженность A версиями функций, в то время как вам необходимо использовать W версии этих функций.

Заменяем всё в lib.rs и main.rs. Пробуем ещё раз.



Победа!

При весе в 164 килобайт на диске, программа занимает 940 килобайт оперативной памяти. И при этом работает с Windows API. Достаточно скромно. Лучше, чем ассемблер с кривыми биндингами, который работает только в консоли.

Итак, давайте пройдёмся по основным моментам этой главы:

  1. Вызов WinAPI это всегда unsafe вызов.
  2. fn main должна возвращать результат, понятный Windows.
  3. Вы знаете где и как найти документацию по Windows API и можете использовать их в вашей программе. (Все данные о WinAPI можно достать здесь, а данные о функциях, спортированных в rust находятся здесь.


▍ Пишем программу


Ну что же, теперь мы умеем создавать программу на rust, которая может тянуть WinAPI. Дело за малым — создать окно, понакидать в него что-то, что будет представлять тайлы в нашей игре, и написать логику.

Ну что же, давайте создадим окно и понакидаем в него всякого разного.

Для начала, давайте обновим build.rs и добавим кое-какие функции, которые нам понадобятся.

fn main() {
    windows::build! {
        Windows::Win32::{
            Foundation::*,
            Graphics::Gdi::ValidateRect,
            UI::WindowsAndMessaging::*,
            System::LibraryLoader::{
                GetModuleHandleA,
            },
        },    
    };
}

После этого заменим импорты и код Main на следующий метод:

unsafe {
        let instance = GetModuleHandleA(None);
        debug_assert!(instance.0 != 0);

        let window_class = "window";

        let wc = WNDCLASSA {
            hCursor: LoadCursorW(None, IDC_ARROW),
            hInstance: instance,
            lpszClassName: PSTR(b"window\0".as_ptr() as _),
            style: CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: None,
            ..Default::default()
        };

        let atom = RegisterClassA(&wc);
        debug_assert!(atom != 0);

        CreateWindowExW(
            Default::default(),
            window_class,
            "2048",
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            None,
            None,
            instance,
            std::ptr::null_mut(),
        );
        Ok(())
    }

Здесь мы вызываем GetModuleHandleA. Эта функция возвращает нам называние модуля в котором мы работаем. Каждое окно должно быть привязано к определённому модулю. В данном случае это будет название нашего бинарного файла.

После этого создаём структуру WNDCLASSA и заполняем её параметрами, с которыми наше окно будет запускаться.

Далее, мы регистрируем этот класс.

После чего мы запускаем это окно, вызывая CreateWindowExW.

Компилируем и пробуем всё это запустить.

На экране нет ничего. Более того, если мы попробуем перекомпилировать проект, то у нас всё рухнет и ругнётся на линкер с ошибкой, что конечный бинарный файл для записи недоступен.

Идём в диспетчер задач и видим там, что наша программа безбожно висит.
Как сказали Ильф и Петров: «Скоро только кошки родятся!» Создание окна в Windows это не просто вызов какой-то там функции. Для правильной работы окна вам потребуется создать обработчик очереди сообщений.
Основная и очень большая статья о том, как работают очереди сообщений в Windows, находится здесь.

Но, для того чтобы дать вам понять, как работает очередь сообщений в Windows, давайте приведём следующий пример.

Сколько раз вам приходилось видеть вот такое?



Думаю много раз. Что произошло? Окно перестало отвечать на сообщения в очереди сообщений.

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

Именно поэтому наша программа превратилась в программу-невидимку.

Давайте создадим обработчик сообщений.

Первым делом добавляем после CreateWindowExW следующий код

let mut message = MSG::default();
while GetMessageW(&mut message, HWND(0), 0, 0).into() {
      DispatchMessageA(&mut message);
}

А в описании структуры WNDCLASSA заменим:

lpfnWndProc

с

None

на

Some(wndproc)

Ну, и теперь, давайте напишем эту wndproc, которая будет у нас обрабатывать сообщения.

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message as u32 {
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}

Собственно говоря, мы просто отправили все сообщения на обработчик сообщений по умолчанию.

Теперь давайте запустим и проверим всё это.

Ура! Наконец-то! У нас есть окно!



Только не пытайтесь его закрыть. У вас всё рухнет. Окно закроется, а программа останется висеть в памяти. И не пытайтесь изменять размер этого окна. Если вдруг чего, вы увидите чёрные прямоугольники, вместо контента.

Сообщений, которые будут посылаться вашему окну очень много. Два основных сообщения, с которыми нам нужно будет разобраться прямо сейчас это WM_PAINT и WM_DESTROY. Первое вызывается, когда окну нужно перерисовать контент, второе — когда окно закрывается. В первом случае мы просто перерисуем контент, во втором — выйдем из программы.

Обновляем код и получаем:

extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
    unsafe {
        match message as u32 {
            WM_PAINT => {
                ValidateRect(window, std::ptr::null());
                LRESULT(0)
            }
            WM_DESTROY => {
                PostQuitMessage(0);
                LRESULT(0)
            }
            _ => DefWindowProcA(window, message, wparam, lparam),
        }
    }
}

При закрытии окна мы выходим из программы, а при изменении размера мы его перерисовываем. Теперь окно не будет виснуть, и вы можете запустить и закрыть программу.

Проверяем. Бинарник размером в 160 килобайт, занимает 1 мегабайт оперативной памяти.

Очень аккуратно.

Проверяем знания:

  • Для работы с окнами необходимо сначала создать окно. Для этого надо найти название модуля, который создаёт окно, создать структуру, описывающую класс окна, зарегистрировать этот класс и создать окно
  • Само по себе окно — это не программа. Необходимо создать обработчик очереди сообщений этого окна.
  • В обработчике нужно создать как минимум логику сообщения WM_DESTROY, чтобы пользователь мог выйти из вашей программы.
  • Несмотря на всё вышеописанное, программа всё равно остаётся компактной.

(Задание внимательным — это окно не будет выводить нормально текст в заголовке на русском. Посмотрите на данные из предыдущей главы, и попробуйте это исправить.)

▍ Что дальше?


На этом я остановлюсь, ибо статья продолжает расти и вылезает за пределы разумного времени прочтения. Хотя вы уже имеете на руках готовую программу, написанную на rust, которая создаёт окно и запускает очередь сообщений. Тут вас остановить может только ваше воображение.

  1. WM_KEYDOWN Сообщение, которое нужно перехватить в нашем обработчике очереди сообщений. Возникает при нажатии на клавишу клавиатуры. В этом сообщении вы получите состояние клавиатуры и сможете отловить нажатие стрелочек.
  2. XInput Если у вас есть игровой контроллер, то вы можете воспользоваться этими API для того, чтобы получить нажатие кнопок управления.
  3. GDI+ позволяет вам рисовать объекты в окне.
  4. Direct2d позволяет это делать намного быстрее и качественнее
  5. A Direct3d12 позволяет вам делать это всё на новом уровне.

Дополнительная информация выложена на github. Там же вы сможете найти огромное количество примеров, включающих все вышеописанные методы отображения контента и ссылки на документацию.

И всё это доступно прямо сейчас в rust. Надеюсь вы узнали для себя немного нового и возможно у вас появились идеи о том, как вы могли бы это применить в вашей работе. Что же, если это так, то я очень рад, что вам пригодилось.

И, как обычно, конкурс. На этот раз задача будет отстоять в реализации самой компактной игры 2048 написанной на rust. Первое место — эквивалент $25 в пейпал. Второе место — $15, третье $5.

Мерять будем потребление оперативки. Игра должна быть написана на rust и в ней обязательно использовать winapi. Она должна быть графической. Но конкретный движок на выбор.

Если вам хочется ещё — то пишите и задавайте вопросы в комментариях.

Для тех, кто пришёл посмотреть на кота, и кому ни rust, ни Windows API не нужны
Кота зовут Мистер Кайден. Живёт он по этому адресу. Это корейский кот, его хозяйка превратила его в знаменитость в Корее. Кот является главным героем манги Eleceed. Рекомендую подключиться к прочтению. Русского перевода я не нашёл, но всё хорошо переведено на английский. Если вы хотите подучить язык — самое то, ибо текста не так много, а кот очаровательный.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 60: ↑56 и ↓4+52
Комментарии31

Публикации

Информация

Сайт
ruvds.com
Дата регистрации
Дата основания
Численность
11–30 человек
Местоположение
Россия
Представитель
ruvds