Недавно я написал статью Трясём стариной — или как вспомнить Ассемблер, если ты его учил 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. Рекомендую подключиться к прочтению. Русского перевода я не нашёл, но всё хорошо переведено на английский. Если вы хотите подучить язык — самое то, ибо текста не так много, а кот очаровательный.

Комментарии (31)


  1. MFilonen2
    06.10.2021 14:28
    +3

    Вызов unsafe не запрещен конечно, но вот одна из задач разработчика в Rust – изолировать такие конструкции, гарантировав на уровне обертки безопасность вызова.
    Поскольку же Windows API один сплошной unsafe, от использования Rust тут, как по мне, мало толку.


    1. snuk182
      06.10.2021 15:54
      +2

      Зависит от задачи. Если вам нужен нативный гуй под окна, никуда от WinAPI не деться. WinUI до продакшена еще не доплыл, а обертки разной степени скисшести все равно обмазывают внутренности тех же вызовов ансейфом.


      1. Nurked Автор
        06.10.2021 17:31

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

        Но как только ты выходишь за пределы уютненького, то тут как не крути, без unsafe разобраться нельзя.


        1. snuk182
          06.10.2021 18:02
          +1

          Можно напилить максимально идиоматичный гуй прямо на Rust. Он будет, вестимо, что-то за собой тащить - графресурсы, OpenGL, логику взаимодействия и отрисовки, и выглядеть как инопланетянин.


          1. Nurked Автор
            06.10.2021 18:36

            Простите, может быть я не знаю правильной библиотеки, но вот опять-же. Читаю про опенгжель на расте, и нахожу следующее:

            Why “unsafe” exists

            It may seem strange that “unsafe” exists at all. The reason for it is quite simple: it allows us to deal with complicated stuff once, inside a function with a safe API, and then completely forget about it when we become the users of that API. In other words, it moves the responsibility of correct API usage to API implementer.

            То есть всё то же самое. Или я где-то пропускаю rust-native opengl?


            1. snuk182
              06.10.2021 18:43
              +2

              Не совсем. Если весь гуй написан на Rust, то на unsafe остаются только неотвратимые системные вызовы: окно создать, кадр нарисовать, кнопки послушать. Какой-бы ни была Rust-прокладка, она все равно на каждый вызов упирается в итоге в unsafe FFI. Просто в гуе, нарисованном на Rust, таких вызовов будет сильно меньше, чем в системном гуе, который из Rust вызывается.


              1. Nurked Автор
                06.10.2021 18:48

                Вы так считаете?

                Ну вот посмотрите, у вас есть процедура обработки сообщений которая unsafe. Из неё вы просто вызываете safe функции да и всё.

                А писать алгоритм изменения размера окна на открытой гжели вручную, тут вам как ни крути, намного больше FFI потребуется.


                1. snuk182
                  06.10.2021 19:22
                  +3

                  Если я рисую все потроха сам, у меня есть только один ffi вызов - в самом конце, для отправки уже нарисованного кадра в систему для отображения. Если я использую системный гуй, у меня получится минимум один ffi на каждый системный контрол.

                  Можно придраться, что для низкоуровневого рисования надо сильно больше одного ffi на кадр. Для окна с одной кнопкой - возможно ffi будет больше у саморисовки, за счет настройки графики, кадра, итп. Если кнопок под сотню, как в микрософт офисе, перевес легко может быть в другую сторону.


                  1. Nurked Автор
                    06.10.2021 21:00

                    Хммм.. А у вас нет проекта в гитхабе, чисто чтобы посмотреть?


                    1. snuk182
                      06.10.2021 23:14
                      +3

                      Для саморисовки через WinGDI вот. Это вообще весь код, количество вызовов ffi постоянно и не зависит от количества рисуемого.

                      https://github.com/snuk182/nuklear-backend-gdi/blob/master/src/lib.rs

                      Для системных контролов вот. Оцените количество ансейфа.

                      https://github.com/plyhun/plygui-win32


                      1. Nurked Автор
                        06.10.2021 23:28

                        Ваш аргумент считаю достойным и удовлетворительным. Спасибо за код.


                      1. snuk182
                        06.10.2021 23:50
                        +2

                        Блин. Прошу меня извинить. Из всех вариантов отрисовки Nuklear я выбрал самый неудачный. В случае именно WinGDI количество ffi таки зависит от количества контролов, потому что графпримитивы контролов рисуются средствами GDI, зато на кадр влияния минимум, а должно быть наоборот - сначала весь гуй компонуется в битмап кадра, а потом сам кадр отправляется через ffi на отрисовку. Правильно сделано вот здесь (биндинги к OpenGL):

                        https://github.com/snuk182/nuklear-backend-glium/blob/master/src/lib.rs

                        Также тут (биндинги к Vulkan/DX11).

                        https://github.com/snuk182/nuklear-backend-wgpurs/blob/master/src/lib.rs


    1. Nurked Автор
      06.10.2021 17:26
      +2

      А как же логика вашей программы?

      Winapi существовал со времён царя Гороха. Обычно, повторюсь, обычно, он не падает. Когда вы последний раз видели падение из за нажатия на кнопку. (не кода самой кнопки, а именно Windows который эту кнопку визуально опускает вниз на экране и поднимает)

      Плюс, опять же, каковы ваши альтернативы? Писать на сях? А потом сидеть и пытаться выяснить, в каком pinvoke вы не разыменовали шестой указатель в какую процедуру, или где не добавили очередную обёртку, которая преобразовала бы вашу строку во что-то ещё?

      Или у вас есть стабильная, проверенная временем система работы с GUI в Rust? Или мы щас вот прямо Electron туда запихнём?


  1. cepera_ang
    06.10.2021 17:32
    +1

    Это всё идёт к вот этому: https://github.com/mcountryman/min-sized-rust-windows, а дальше к этому https://keyj.emphy.de/win32-pe/ и в пределе получается https://github.com/pts/pts-tinype.


    1. Nurked Автор
      06.10.2021 17:57
      +1

      А скажите, каким Макаром это связано со статьёй? Писать маленькие бинарники это было потехой со времён бояр при Иване Грозном. Но они же ничего не делают.

      А вот что-то работающее - это другое дело.


      1. cepera_ang
        06.10.2021 18:21

        Так это же вы аж конкурс объявили на написание самого маленького бинарника :)


        Любой желающий участвовать довольно быстро обнаружит, что самый маленький дефолтный бинарник на расте получается размером 2кб и туда с лихвой поместится логика из прошлой статьи, а чтобы уменьшать дальше понадобятся уже хитрые трюки, вот тут-то и пригодятся мои ссылки :) Особенно рекомендую вторую — это не раст, но эпос просто замечательный и там бинарник не просто пустой, а ещё и делает небольшую, но полезную функцию с клипбордом. Может его на хабр перевести?


        1. Nurked Автор
          06.10.2021 18:42

          Но я то объявил конкурс на самый маленький футпринт в памяти. А не на диске. Так что тут такие дела. Сколько она на диске займёт мне не важно. Плюс, одно из условий конкурса - это графический интерфейс.

          Возможно можно будет стандартными шейдерами DirectX запилить его. Вот тут, если помните в 2004 году была .kkrieger?

          А с переводом — почему бы и нет? Вы читать будете? Если да, то я переведу 8-)


          1. cepera_ang
            06.10.2021 19:03

            А, действительно, это я как-то обчитался и переключился на размер бинаря, а не в оперативке :) Но тут тоже сложности — что считать потреблением оперативной памяти? Working set? Active working set? Committed memory? И в любом случае минимум будет в пару сотен килобайт под обязательные структуры процесса + дефолтные загруженные библиотеки, даже если сам код весь в регистры распихать.


            Насчёт статьи — я её с удовольствием в оригинале прочитал, так что перевод читать не буду (разве что писать :) ) Но вот что люди хотят читать на хабре мне чем дальше, тем становится всё менее понятно — кажется большей популярностью пользуются нетехнические набросы, а не хардкорные дебри (или даже софткорные).


            1. Nurked Автор
              06.10.2021 19:14
              +2

              Ну, по этому поводу, я могу вам сказать одно: Я как писал про код, так и буду писать про код.


  1. TrueBers
    07.10.2021 01:21
    +10

    Ох, из чего ж статью то высосали, а?

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

    unsafe в этом случае нужен для того, чтобы сделать safe-обёртку, RAII и т.п. Ваш же код ни на йоту не безопаснее кода на Си. Даже наоборот: unsafe во всех местах, а не в конкретных идиоматических -- верный генератор UB похлеще С++.

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

    Что-то поменялось? windows-rs всегда без проблем работал со stable.

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

    Цель этой библиотеки -- вынести биндинги в отдельный юнит для компилятора, чтобы каждый раз не запускать кодогенерацию и компиляцию. И не подключает она никаких зависимостей, она генерирует из метаданных обёртки, типы и адаптеры для удобного вызова extern функций. И да, это не биндинги в привычном их понимании, а именно wrapper'ы, которые, по опыту использования, очень хреново инлайнятся и кучу неявных side-эффектов генерят. Например, получая адрес функции, получаешь адрес незаинлайненного thumb wrapper'а, даже если фактический вызов заинлайнился, то есть, по сути, адрес мёртвого кода. При этом адрес получить можно только полным повторным extern fn объявлением для каждой WinAPI функции. Конечно, адреса нужны не всегда, но иногда нужно и много, не зря же Раст -- всё таки системный низкоуровневый язык.

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

    Опять таки, никаких зависимостей он не подключает, а просто инлайнит сгенерированный из метадаты исходник на Расте из стандартной директории выхлопа генератора.

    Первое — функция main теперь должна возвращать windows::Result<()>

    2. fn main должна возвращать результат, понятный Windows

    Не выдумывайте, откуда вы взяли такое требование? main может возвращать любой тип, который реализует трейт std::process::Termination, хоть пустую юнит-стуктуру делайте и возвращайте из неё 42, никто не запрещает. Похоже, вы скопипастили из примеров код, не разобравшись, зачем там этот тип указан. А указан он для использования оператора ? на std::ops::Try-типах, то есть там, где возвращается либо COM'овский Result, содержащий WinRT'шную ошибку, либо растовый Result, полученный через какой угодно метод конвертации. Вы же ни разу этот оператор не использовали, значит тип возврата main тут не причём и может быть любым impl Termination.

    Пишете, что нужно везде использовать W-версии функций, а сами используете A-версии.

    debug_assert!(instance.0 != 0);

    Доступ в кишки тупла -- такое себе. В windows-rs эта проверка делается через windows::Handle::is_invalid().

    let atom = RegisterClassA(&wc);

    deprecated же. RegisterClassEx сейчас рекомендуется использовать.

    HWND(0)

    То, что HWND == NULL и HWND == 0 имеют одинаковые значения -- чисто случайное implementation defined совпадение и потенциальное UB. В windows-rs трейт std::default::Default мапится на NULL handle.

    extern "system" fn wndproc(...){ unsafe{...

    Вся функция здесь -- unsafe. Нужно не блок внутри функции делать unsafe, а всё extern-объявление, ибо unsafe интерфейс и unsafe реализация -- разные вещи.

    match message as u32

    В чём сакральный смысл конвертации u32 в u32?

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

    Поэтому нам не нужно извращаться и писать что-то типа:

    MessageBoxA(std::ptr::null_mut(), ...

    Здесь не стоит путать ABI указателя на ffi функцию и его мапинг из Option'а с костылём, который придумали в windows-rs. Это совершенно разные вещи. Из коробки работает толькоOption<extern fn()>. Другие типы без бубнов так не работают, нужны обёртки из трейтов.


    1. ratijas
      07.10.2021 10:44
      +2

      по фактам.


  1. Nurked Автор
    07.10.2021 06:36
    -4

    Уважаемый, вводная же статья. Первая доза бесплатно. Зачем распугиваешь всех? Оно понятно что про дебри FFI и возвращаемые значения в виде писать можно долго. Но не прям с карьера.

    Приходите, пишите. Будет полезно.


    1. enabokov
      07.10.2021 10:50
      +4

      Всё же очень полезные замечания. Не надо так.


  1. enabokov
    07.10.2021 10:53
    +1

    На Win32 API писать скучно, муторно и одноплатформенно. Вот интересно на Sciter или Tauri.


    1. bonta
      07.10.2021 12:03
      -2

      из-за таких как вы современный софт деградирует.


      1. cepera_ang
        07.10.2021 13:19
        +2

        Зато, благодаря вам, культура общения в профессиональном сообществе на высоте.


  1. milyin
    07.10.2021 12:10
    +2

    Спасибо за статью! Как правильно написал TrueBers, стоило упомянуть, что windows-rs базируется на подходе language projection - автоматическая генерация идиоматической для языка библиотеки из метаданных - который применяется также для представления Windows API в C++ и в С#. То есть это позволяет надеяться, что проект останется актуальным надолго.

    Про nightly у меня тоже большие сомнения - я конечно на последнюю версию windows-rs давно не переходил, но с чего бы им от stable отказываться?

    Ну и про 2048 - вот для затравки мой вариант https://github.com/milyin/game2048-rs :-)


    1. snuk182
      08.10.2021 11:24
      +1

      Вот интересно. Здесь `windows-rs` наколбасил генерированных биндингов, и отдает их все сразу. У соседнего крейта `winapi-rs` подход к линковке другой - надо явно указать в фичах неймспейс, и тогда он будет подлинкован. Насколько такой подход с разделением оправдан?


      1. milyin
        08.10.2021 12:39
        +1

        winapi-rs я не трогал, но думаю, что он пишется руками. Т.е. там будет только то, что разработчики написали.

        У windows-rs же подход другой - они генерируют обертку к любым COM-объектам, API которых описан в .winmd файлах. Например у себя в game2048-rs я подключаю win2d прямо из DLL-ей, которые лежат вместе с проектом - windows-rs подхватывает их из https://github.com/milyin/game2048-rs/tree/master/.windows.


        1. Nurked Автор
          08.10.2021 18:29

          Хмммм.... Хмммм.... Лезем разбираться. Красиво у вас получается. Вы, судя по всему, взялись за получение первого места? :-)


          1. milyin
            08.10.2021 18:51

            Конечно - тем же способом, как до этого C++ за 21 день выучил - https://habr.com/ru/post/87737 :-)