![](https://habrastorage.org/webt/_i/5f/bi/_i5fbiwfsoqrwxdz0k0v6pye6gw.png)
Недавно я написал статью Трясём стариной — или как вспомнить Ассемблер, если ты его учил 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.
Для начала, нам потребуется 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(())
}
Смотрим: Потрачено!
![](https://habrastorage.org/webt/3d/0x/jk/3d0xjkq1gg5sx8firvmeno6imiw.png)
Замечательно! Повсюду кракозябры, как в 1999 году. Что делать? Разбираемся и понимаем, что MessageBoxA работает c ANSI строками, в то время как MessageBoxW работает с Unicode (Wide strings). Собственно говоря, эта нотация сохранилась во всех Windows API. Я не знаю причин не использовать W версии функций. Но, будьте аккуратны, большое количество писателей мануалов на английском языке не понимают разницы, и вы увидите загруженность A версиями функций, в то время как вам необходимо использовать W версии этих функций.
Заменяем всё в lib.rs и main.rs. Пробуем ещё раз.
![](https://habrastorage.org/webt/ev/3-/zv/ev3-zvetzllye_wyubek05gvgty.png)
Победа!
При весе в 164 килобайт на диске, программа занимает 940 килобайт оперативной памяти. И при этом работает с Windows API. Достаточно скромно. Лучше, чем ассемблер с кривыми биндингами, который работает только в консоли.
Итак, давайте пройдёмся по основным моментам этой главы:
- Вызов WinAPI это всегда unsafe вызов.
- fn main должна возвращать результат, понятный Windows.
- Вы знаете где и как найти документацию по 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, давайте приведём следующий пример.
Сколько раз вам приходилось видеть вот такое?
![](https://habrastorage.org/webt/4e/5b/58/4e5b58jq082vlvlbd7c5twjm-ku.png)
Думаю много раз. Что произошло? Окно перестало отвечать на сообщения в очереди сообщений.
Фактически, у вас постоянно должна работать неблокируемая функция, которая будет получать и обрабатывать сообщения, посылаемые в окно. Пример того, что будет, если вы умудритесь заблокировать эту функцию приведён выше. Наше окно не отвечает на системные запросы.
Именно поэтому наша программа превратилась в программу-невидимку.
Давайте создадим обработчик сообщений.
Первым делом добавляем после 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),
}
}
}
Собственно говоря, мы просто отправили все сообщения на обработчик сообщений по умолчанию.
Теперь давайте запустим и проверим всё это.
Ура! Наконец-то! У нас есть окно!
![](https://habrastorage.org/webt/ig/8j/nm/ig8jnmktr7feyjwvvjoyscgpj9s.png)
Только не пытайтесь его закрыть. У вас всё рухнет. Окно закроется, а программа останется висеть в памяти. И не пытайтесь изменять размер этого окна. Если вдруг чего, вы увидите чёрные прямоугольники, вместо контента.
Сообщений, которые будут посылаться вашему окну очень много. Два основных сообщения, с которыми нам нужно будет разобраться прямо сейчас это 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, которая создаёт окно и запускает очередь сообщений. Тут вас остановить может только ваше воображение.
- WM_KEYDOWN Сообщение, которое нужно перехватить в нашем обработчике очереди сообщений. Возникает при нажатии на клавишу клавиатуры. В этом сообщении вы получите состояние клавиатуры и сможете отловить нажатие стрелочек.
- XInput Если у вас есть игровой контроллер, то вы можете воспользоваться этими API для того, чтобы получить нажатие кнопок управления.
- GDI+ позволяет вам рисовать объекты в окне.
- Direct2d позволяет это делать намного быстрее и качественнее
- A Direct3d12 позволяет вам делать это всё на новом уровне.
Дополнительная информация выложена на github. Там же вы сможете найти огромное количество примеров, включающих все вышеописанные методы отображения контента и ссылки на документацию.
И всё это доступно прямо сейчас в rust. Надеюсь вы узнали для себя немного нового и возможно у вас появились идеи о том, как вы могли бы это применить в вашей работе. Что же, если это так, то я очень рад, что вам пригодилось.
И, как обычно, конкурс. На этот раз задача будет отстоять в реализации самой компактной игры 2048 написанной на rust. Первое место — эквивалент $25 в пейпал. Второе место — $15, третье $5.
Мерять будем потребление оперативки. Игра должна быть написана на rust и в ней обязательно использовать winapi. Она должна быть графической. Но конкретный движок на выбор.
Если вам хочется ещё — то пишите и задавайте вопросы в комментариях.
Для тех, кто пришёл посмотреть на кота, и кому ни rust, ни Windows API не нужны
Кота зовут Мистер Кайден. Живёт он по этому адресу. Это корейский кот, его хозяйка превратила его в знаменитость в Корее. Кот является главным героем манги Eleceed. Рекомендую подключиться к прочтению. Русского перевода я не нашёл, но всё хорошо переведено на английский. Если вы хотите подучить язык — самое то, ибо текста не так много, а кот очаровательный.
![](https://habrastorage.org/webt/ou/g5/kh/oug5kh6sjydt9llengsiebnp40w.png)