Daniel Drywa

Simple Windows API Window With Rust

We are creating a simple window with Rust on Windows 10

During my Rust learning phase I wanted to see how easy it would be to do a simple windows application. Console applications on Windows are not a problem at all and easy to do. Simply print some characters to the output stream and you’re done. However doing anything more advanced like a GUI or a Game requires it’s own window that we can render to. The modern way to do this on Windows 10 is to utilise the Universal Windows Platform. Unfortunately at the time of writing it is not possible to use the UWP APIs from within Rust. But luckily the Windows API has a simple C interface that you can call from Rust via FFI. Keep in mind however that the Windows API (Previously named Win32) is an old interface and Microsoft is slowly replacing it with the Windows Universal Platform. That means you won’t be able to access store specific features (Or any new windows feature) from the Windows API. Hopefully at some point in the future Microsoft will release some more language interfaces for the UWP so we wouldn’t be constrained to just C++, C#, and JS.

Getting Started

First of all we need new rust project: cargo new win32_windows --bin. Before we go ahead and implement our own Rust Windows API bindings it is a good idea to take a look at crates.io to find out if anyone has already done the hard work for us. And thanks to Peter Atashian and his winapi crate we have some very good Rust Windows API bindings at our disposal. The beauty of that crate is that every module of the Windows API is packed into it’s own crate. To create a simple window you need to add the following crates as dependencies to your project’s Cargo.toml:

Don’t forget to import those crates into your project’s main.rs file. Rust applications on Windows always open a new console window if started by double-clicking on the executable. This is not really what we want though and luckily Rust 1.18 introduced a new attribute to change this behaviour: #![windows_subsystem = "windows"]. This will prevent our application from opening a new console window on every start. You need to add this attribute at the very top of your code file.

Strings and the Windows API

Strings in the Windows API work in two ways. Either they are encoded in basic 8-bit ANSI characters or they use UTF-16 encoding (16-bit characters). Each function that takes a string therefore has two versions. One ending with a capital A for ANSI based strings and one ending with a capital W for wide characters (UTF-16). So in order to interact with the Windows API we need to convert our Rust UTF-8 strings to Windows UTF-16 strings. We are going to need the following imports:

The Windows API only needs a pointer to the beginning of the UTF-16 encoded string. The end of the string is indicated via a 0 value at the end. Therefore the most convenient way to pass a string to the Windows API functions is as a plain vector of u16. We don’t need to keep the OsStr slice around for any operations. All string operations should be done with Rusts default UTF-8 encoded string Or str types and you should only convert them to wide strings if you are about to pass them to the Windows API. This will remove any headaches and compatibility issues with other Rust functions. Converting a UTF-8 slice to a vector of u16 would be done like this: OsStr::new( "My string" ).encode_wide().chain( once( 0 ) ).collect(). chain( once( 0 ) ) makes sure the resulting wide string is terminated by a zero. collect() collects all u16 values into a vector. For more information see the documentation for chain, once, and collect. We could write a simple helper function like this:

fn win32_string( value : &str ) -> Vec<u16> {
    OsStr::new( value ).encode_wide().chain( once( 0 ) ).collect()
}

Creating a Window

In order to create a window with the Windows API we need to create and register a window class. This class describes the basic properties of our window such as style, background brush, menus, icon, and many more. To define our window we need to use the WNDCLASSW struct located in the winapi::winuser namespace. The style we are going with looks something like this:

let wnd_class = WNDCLASSW {
    style : CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
    lpfnWndProc : Some( DefWindowProcW ),
    hInstance : hinstance,
    lpszClassName : name.as_ptr(),
    cbClsExtra : 0,
    cbWndExtra : 0,
    hIcon: null_mut(),
    hCursor: null_mut(),
    hbrBackground: null_mut(),
    lpszMenuName: null_mut(),
};

First up is the style of the window class. All possible styles can be found in the Windows API docs. We are going with CS_OWNDC, CS_HREDRAW, CS_VREDRAW. These are the most common values. Next up we have lpfnWndProc which is of type WNDPROC. This is a callback for any window event that can occur in our window. Here you could react to events like WM_SIZE or WM_QUIT. However for our window we are not going to do anything special so we can just use the default callback: user32::DefWindowProcW. The third value is the instance handle for our application which we can retrieve by calling GetModuleHandleW which is located in the kernel32 namespace. Then we have our class name which, if you remember, needs to be a UTF-16 string. You can read about all the other parameter in the Windows API docs but they are mostly irrelevant for our purposes, which is why we set them to NULL via std::ptr::null_mut (See docs).

Finally we are able to register our class with Windows by calling RegisterClassW which is located in the user32 namespace. Now that we have registered our window class we are finally able to create a window based on it. To do that we need to call CreateWindowExW which is also located in the user32 namespace.

let handle = CreateWindowExW(
    0,
    name.as_ptr(),
    title.as_ptr(),
    WS_OVERLAPPEDWINDOW | WS_VISIBLE,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    null_mut(),
    null_mut(),
    hinstance,
    null_mut() );

We are passing the name of the class that we want to use for this window, which will be the same that we have registered before. We can also set a window title and a general style (aka does it have minimize/maximize buttons, etc.). WS_OVERLAPPEDWINDOW and WS_VISIBLE describe the most common and basic window. The following CW_USEDEFAULT values are for the window position (x, y) and it’s size (width, height). This will use the system defaults because for now we don’t exactly care, we just want something to show up. The last interesting parameter is the application instance that we have to pass to the window as well. And that’s it. CreateWindowExW will return us a handle to our window of type HWND, which we can pack into our own struct so we can pass it around:

struct Window {
    handle : HWND,
}

Adding some error handling we now have a simple create_window function that will create us a very simple win32 window:

fn create_window( name : &str, title : &str ) -> Result<Window, Error> {
    let name = win32_string( name );
    let title = win32_string( title );

    unsafe {
        let hinstance = GetModuleHandleW( null_mut() );
        let wnd_class = WNDCLASSW {
            style : CS_OWNDC | CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc : Some( DefWindowProcW ),
            hInstance : hinstance,
            lpszClassName : name.as_ptr(),
            cbClsExtra : 0,
            cbWndExtra : 0,
            hIcon: null_mut(),
            hCursor: null_mut(),
            hbrBackground: null_mut(),
            lpszMenuName: null_mut(),
        };

        RegisterClassW( &wnd_class );

        let handle = CreateWindowExW(
            0,
            name.as_ptr(),
            title.as_ptr(),
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            null_mut(),
            null_mut(),
            hinstance,
            null_mut() );

        if handle.is_null() {
            Err( Error::last_os_error() )
        } else {
            Ok( Window { handle } )
        }
    }
}

The main loop

However running the application now will present us with a window that seems to be frozen. No input will have any effect on the window. This is because our application is not hooked with the Windows messaging system yet. See this link for more information. In order to process messages that have been sent to our application we need to get them off the message queue via GetMessageW, translate them into something meaningful via TranslateMessage, and finally dispatch them to the right callback via DispatchMessageW. This would look something like this:

loop {
    let mut message : MSG = mem::uninitialized();
    if GetMessageW( &mut message as *mut MSG, window.handle, 0, 0 ) > 0 {
        TranslateMessage( &message as *const MSG );
        DispatchMessageW( &message as *const MSG );
    } else {
        break;
    }
}

The window.handle passed into GetMessageW is our handle for the previously created window, stored inside our Window struct. If we now launch our application we will have a fully functioning and well behaved window. Well, with the exception of the white background slowly disappearing on resizing the window. This is because we don’t do any real drawing or resizing of the image buffer yet. Typically you would fill the window surface with the colour you want during a WM_PAINT event in the window callback. But this is way out of scope for this post.

Remarks

I hope this little exercise is useful for anyone. I know I have been a little vague with some of the details in this post put I think I gave enough pointers/links that include some more in-depth knowledge. For me it was surprisingly simple to use the winapi crate with a few exceptions like wide strings. The way they need to be converted is not entirely clear from the beginning and most of the functions that deal with OsStr within the winapi crate are not documented or lead to a 404 page. But thankfully there was a short MessageBox example on the GitHub page that helped me out.

I find it really frustrating though that Microsoft is not supporting more languages in their Universal Windows Platform. And that they seem to go for a full C++/Object Oriented API for their future model which, in my book, makes things just way more complicated. I would wish for a simple C style interface that we could call into, but I think my calls will go unanswered.

The Full Code

The full code is available at GitHubGist: gist.github.com


02 July 2017