GitHub Repo

Triangle From Scratch: Introduction

This is an educational series about drawing a triangle without using any outside crates.

Specifically, the rules are:

  1. We can only put a crate into the [dependencies] section of Cargo.toml if it's a crate that we wrote ourselves, as part of this project.
  2. We can still use Rust's standard library. Since all Rust programs can import from the standard library without a [dependencies] entry, it's fair game.

Without any external crates, we'll have to write our own operating system bindings. It's not difficult code to write, there's just a lot of background details you need to understand first. That's where most of our focus will go, on learning how that works. There's a lot less focus spent on the literal "triangle drawing" part, which is usually fairly easy.

Expected subjects include:

  • Reading OS documentation (which usually assumes you're programming in C).
  • Understanding the C header files that describe the OS's public API.
  • Writing appropriate "raw" Rust bindings to that public API.
  • Creating ergonomic wrapper functions to make the API easily used with the rest of Rust.
  • Having those wrapper functions be fully safe (in the Rust sense) when possible, or at least making them as error-proof as we can.

I will be frequently linking to the documentation pages many of the the important things we deal with. I expect you to actually go to those pages and read what they say. I will be covering much, but I can't cover every single detail every single time. You should always read the documentation for yourself as well as reading this tutorial.

Reminder: The "absolutely no dependencies" thing is for demonstration purposes only. If you actually want to draw a triangle within a reasonable amount of development time, please do feel free to use dependencies. Depending on what you need to do, there's generally many good crates available.

Opening A Window

If we wanna draw a triangle, we have to have some place to put a triangle.

Generally (but not always) this means showing it in a graphical window on the screen.

Because operating systems all support more than one drawing API, and because many drawing APIs can be used with more than one operating system, we're going to separate the lessons about opening a window from the lessons about using a particular drawing API.

Opening a Win32 Window

On Windows, the C oriented API is called "Win32".

There's also some C++ oriented APIs for interacting with Windows as well (called COM and WinRT).

It's much easier for Rust to bind to and interact with a C oriented API than a C++ oriented API, so we'll start with just using Win32 to interact with Windows.

Search The Web

Okay, for the sake of the lesson let's pretend that even I don't know what to do.

Let's start by asking the internet nicely. Something like "open a window win32" sounds right. Hey look, that first result is straight from Microsoft. It's a whole little tutorial on how to open a window. Perfect, just what we wanted.

Starting The Win32 Windowing Tutorial

Let's read the first paragraph of the windowing tutorial that we just found...

To summarize the opening portion:

  • Every window needs a window class.
  • A window class is registered with the OS at runtime.
  • We need to fill in a WNDCLASSA (or WNDCLASSW)

Whoa, slow down, hold on, what's this structure thing? And why are there two versions?

ANSI and Wide

All over the Win32 API you'll find stuff where there's an A version and a W version. This happens with functions that process textual data, as well as with structs associated with those functions. In this case of WNDCLASSA / WNDCLASSW, a window class has, as part of it, a menu name as well as a class name. These names are textual, and so we get both an A and a W version.

The A and W letters come from the two types of string that the windows API lets you use: ANSI strings and "wide" strings.

  • ANSI strings use C's char type. They don't have a specified encoding. If you store anything other than ASCII data in an ANSI string, the results vary based on context.
  • Wide strings use C's wchar_t type. These strings are UTF-16 encoded. This gives you consistent results while using all the world's writing systems.

What does this mean for us Rust users?

Well, Rust string literals, and Rust's normal String and &str types, are all UTF-8 encoded. This means there's a bit of a mismatch between what Windows expects and what we've usually got.

UTF-8 is a superset of ASCII. This means that any ASCII-only string can be stored compatibly inside a UTF-8 string. So if we only want to use ASCII data the normal String and &str types will be (mostly) compatible with A-type operations.

On the other hand, ASCII is pretty limited. Most languages of the world aren't representable with only ASCII text. You get English, Latin, Esperanto, Klingon, but the list runs out quick after that. Even English doesn't get all of its fancy typographical marks in an ASCII-only context: ellipses (…), “angled quotes”, different length dashes (– / —), and so on.

So we really want to be using these W-type operations. This means that we have to convert UTF-8 over to UTF-16. Oh, hey, look, that's in the standard library, isn't it neat? The only slight problem is that we can't use that in a const context (yet). It's not the worst to do a little runtime data mucking here and there, so we'll accept the overhead. The UTF-16 conversion is kinda just an "unfortunate but accepted" part of working with Windows.

Reading a C struct declaration

Okay, so now we've picked that we're going to use WNDCLASSW. Let's look at the MSDN definition:

typedef struct tagWNDCLASSW {
  UINT      style;
  WNDPROC   lpfnWndProc;
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCWSTR   lpszMenuName;
  LPCWSTR   lpszClassName;
} WNDCLASSW, *PWNDCLASSW, *NPWNDCLASSW, *LPWNDCLASSW;

Oh, gross, what the heck? What's going on here? Let's take it one part at a time.

  • typedef says that we're making a "type definition". The way it works is that first you give a base type, and then you list one or more other names you want to have as aliases.
  • struct tagWNDCLASSW this names the first type, that we're making the aliases for.
  • { ... } the part in braces lists the fields of the struct. Each line has the field's type, then the name of the field, then a ;
  • WNDCLASSW, is the first alias we're making. From now on, if you refer to a WNDCLASSW, then it's the same as if you'd referred to the whole struct tagWNDCLASSW { ... } declaration. This is really good, because writing out all the fields any time we just want to talk about the type is just a pain.
  • *PWNDCLASSW, *NPWNDCLASSW, *LPWNDCLASSW; these are more aliases as well. The * makes these pointer types, so a PWNDCLASSW is the same as struct tagWNDCLASSW { ... } * or WNDCLASSW*. The prefixes on each name variant stand for "Pointer", "Near Pointer", and "Long Pointer". Long ago when computers had segmented memory there were differences in the pointer types. These days computers aren't set up for that, so they're all just a normal pointer. The different names are still around for legacy compatability.

Starting Our Rust Code

I think we've got enough on our plate to start writing things down in Rust.

Microsoft Windows [Version 10.0.19041.685]
(c) 2020 Microsoft Corporation. All rights reserved.

D:\dev\triangle-from-scratch>cargo init --bin
     Created binary (application) package

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.65s
     Running `target\debug\triangle-from-scratch.exe`
Hello, world!

Great. Later on we'll put some of this into a library, sort it into modules, all that sort of housekeeping stuff. For now, we'll just write into main.rs.


#![allow(unused)]
fn main() {
#[repr(C)]
struct WNDCLASSW {
  style: UINT,
  lpfnWndProc: WNDPROC,
  cbClsExtra: int,
  cbWndExtra: int,
  hInstance: HINSTANCE,
  hIcon: HICON,
  hCursor: HCURSOR,
  hbrBackground: HBRUSH,
  lpszMenuName: LPCWSTR,
  lpszClassName: LPCWSTR,
}
}

Oh, excellent, and we're sure to put that little repr(C) at the top. This makes sure it has the right memory layout for interacting with foreign code.

Let's give that a try:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
error[E0412]: cannot find type `UINT` in this scope
 --> src\main.rs:9:10
  |
9 |   style: UINT,
  |          ^^^^ not found in this scope

error[E0412]: cannot find type `WNDPROC` in this scope
  --> src\main.rs:10:16
   |
10 |   lpfnWndProc: WNDPROC,
   |                ^^^^^^^ not found in this scope

...you get the idea

Okay, so, that should be obvious enough in terms of the error message. We can't declare a struct to have fields with types Rust doesn't know about. It's just not gonna fly.

How Big Is An Int?

Okay, start with just the first field on the list of missing types. Another web search for "msdn uint", and we find a handy page of Windows Data Types.

UINT: An unsigned INT. The range is 0 through 4294967295 decimal.

This type is declared in WinDef.h as follows:

typedef unsigned int UINT;

Alright, closer to an answer. Now we just ask "how big is an int on windows", which doesn't have any pages that immediately look useful. What if we ask "how big is an int on windows msdn"? Ah, here we go, Data Type Ranges gives us all the info we need about the size of different C types.

An unsigned int is 4 bytes, so in Rust terms it's a u32. We could call our type unsigned_int, but the rust convention is to give C types a c_ prefix, and also to just say u for "unsigned". In other words, unsigned int in C becomes c_uint in the Rust convention. There's no strong reason to not keep with this naming convention, so we'll go with that.

Now we can add definitions that get us up to UINT, and we can do signed ints as well while we're at it:


#![allow(unused)]
fn main() {
#[repr(C)]
struct WNDCLASSW {
  style: UINT,
  lpfnWndProc: WNDPROC,
  cbClsExtra: c_int,
  cbWndExtra: c_int,
  hInstance: HINSTANCE,
  hIcon: HICON,
  hCursor: HCURSOR,
  hbrBackground: HBRUSH,
  lpszMenuName: LPCWSTR,
  lpszClassName: LPCWSTR,
}
type UINT = c_uint;
type c_uint = u32;
type c_int = i32;
}

Three of the fields aren't underlined in red already!

Reading a C function declaration

Now we look up WNDPROC, which is a WindowProc callback function:

LRESULT CALLBACK WindowProc(
  _In_ HWND   hwnd,
  _In_ UINT   uMsg,
  _In_ WPARAM wParam,
  _In_ LPARAM lParam
);

Oh, no, we're back to the weird stuff again!

Really, it's not too bad. We do need a few hints though:

  • _In_ is just a note on the intended usage of that function argument. It's a C macro which gets replaced with nothing later on, so it's basically a code comment. These arguments move data "in" to the function. Sometimes there's "out" arguments, or even "in-out" arguments. We'll worry about those later.
  • CALLBACK is a C macro that gets replaced with the "callback" ABI attribute. In this case, that's __stdcall. How do I know that? Well, I had to look directly in the windows include files. Unfortunate, but occasionally necessary. If you have visual studio installed, it should be in something like C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0. Then I just did a grep to look for CALLBACK and looked around. Lots of false hits, but the only one where CALLBACK gets defined as a function attribute is 127:#define CALLBACK __stdcall, so that's our winner. (NOTE: later on I found that CALLBACK is discussed on the Windows Data Types page, so it's much less mysterious than I thought at first. Still, it's good to have a note on where to find the headers, so I'm leaving this bit in here.)

Alright, get that junk out of the way and what do we see?

LRESULT WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

Oh, hey, we can almost read that. It helps to remember that C puts the function output type to the left of the function's name, and also the function argument types are to the left of each argument name. When we think back to how strut fields were declared, this is all fairly consistent.

The final very important thing to know is that C function pointers are nullable, while Rust fn pointers are always non-null. If we want to have a nullable value on the Rust side, we have to use Option<fn()> instead of just fn().

So let's finally add that WNDPROC definition:


#![allow(unused)]
fn main() {
type WNDPROC = Option<
  unsafe extern "system" fn(
    hwnd: HWND,
    uMsg: UINT,
    wParam: WPARAM,
    lParam: LPARAM,
  ) -> LRESULT,
>;
}

VS Code says we're at 12 errors. Not so bad.

Void Pointers

Now that we understand what we're supposed to be doing, it's just a matter of filling in definition after definition until all the errors go away. A lot of them are over on that Windows Data Types page, so we don't even have to look too many places.

Next up is HINSTANCE:

HINSTANCE: A handle to an instance. This is the base address of the module in memory.

HMODULE and HINSTANCE are the same today, but represented different things in 16-bit Windows.

This type is declared in WinDef.h as follows:

typedef HANDLE HINSTANCE;

So


#![allow(unused)]
fn main() {
type HINSTANCE = HANDLE;
}

Next, HANDLE:

HANDLE: A handle to an object.

This type is declared in WinNT.h as follows:

typedef PVOID HANDLE;

This is where it gets interesting, because now we need to have PVOID:

PVOID: A pointer to any type.

This type is declared in WinNT.h as follows:

typedef void *PVOID;

Remember that the * after the type makes it a pointer variant of the type. It also has the P prefix we saw before.

The void type name in C performs a sort of double duty, but in Rust we actually don't see it very often.

  • When void is used as a return type it means that there's no return value from a function. In Rust we instead use the () type for functions that return nothing.
  • When void is used as a pointer target type it means that the pointer points to just some opaque memory. In Rust, we don't really care for mysterious opaque memory, and we have generics, so we essentially never end up using void pointers.

Because the void* type (and the const void * type) are the special memory handling types in C, LLVM has particular knowledge and opinions about how they work. To ensure that Rust has the correct type mapping for void pointers, there's a c_void type provided in the standard library.


#![allow(unused)]
fn main() {
type PVOID = *mut core::ffi::c_void;
}

Pointer Sized Types

As we proceed down the list of errors, filling them in one at a time, things are fairly simple based on what we know to do so far, and we get this:


#![allow(unused)]
fn main() {
type HICON = HANDLE;
type HCURSOR = HICON;
type HBRUSH = HANDLE;
type LPCWSTR = *const WCHAR;
type WCHAR = wchar_t;
type wchar_t = u16;
type HWND = HANDLE;
type WPARAM = UINT_PTR;
}

Then we get to UINT_PTR, which has a slightly funny description:

UINT_PTR: An unsigned INT_PTR.

This type is declared in BaseTsd.h as follows:

// C++
#if defined(_WIN64)
 typedef unsigned __int64 UINT_PTR;
#else
 typedef unsigned int UINT_PTR;
#endif

Hmm, a little confusing. So far the types haven't cared about the architecture size. Maybe something is up. Let's see what INT_PTR says:

INT_PTR	
A signed integer type for pointer precision. Use when casting a pointer to an integer to perform pointer arithmetic.

This type is declared in BaseTsd.h as follows:

// C++
#if defined(_WIN64) 
 typedef __int64 INT_PTR; 
#else 
 typedef int INT_PTR;
#endif

Ah ha, so INT_PTR is the signed integer type used for pointer arithmetic, and UINT_PTR is the unsigned version of course. Well, if they're for pointer math, that's why they care about the size of a pointer. If you know your Rust types then you already know what we need to use. That's right, isize and usize. They're naturally the size of a pointer, and there's the signed and unsigned variants and everything.

And now we can finally get no errors with our struct declaration!


#![allow(unused)]
fn main() {
type c_int = i32;
type c_uint = u32;
type HANDLE = PVOID;
type HBRUSH = HANDLE;
type HCURSOR = HICON;
type HICON = HANDLE;
type HINSTANCE = HANDLE;
type HWND = HANDLE;
type LONG_PTR = isize;
type LPARAM = LONG_PTR;
type LPCWSTR = *const WCHAR;
type LRESULT = LONG_PTR;
type PVOID = *mut core::ffi::c_void;
type UINT = c_uint;
type UINT_PTR = usize;
type WCHAR = wchar_t;
type wchar_t = u16;
type WPARAM = UINT_PTR;

type WNDPROC = Option<
  unsafe extern "system" fn(
    hwnd: HWND,
    uMsg: UINT,
    wParam: WPARAM,
    lParam: LPARAM,
  ) -> LRESULT,
>;

#[repr(C)]
pub struct WNDCLASSW {
  style: UINT,
  lpfnWndProc: WNDPROC,
  cbClsExtra: c_int,
  cbWndExtra: c_int,
  hInstance: HINSTANCE,
  hIcon: HICON,
  hCursor: HCURSOR,
  hbrBackground: HBRUSH,
  lpszMenuName: LPCWSTR,
  lpszClassName: LPCWSTR,
}
}

Phew.

Continuing The Windowing Tutorial

I don't know if you recall, but like a decade ago when this article started we had a windowing tutorial that we were working on.

Making a WNDCLASSW value

It says that we need to fill in the window procedure, the hinstance, and the class name. The other stuff is optional, but those are essential.

In the sample C++ code, we see this interesting line:

WNDCLASS wc = { };

That's a little odd looking, it might not be obvious what's happening. It's declaring a variable wc, of type WNDCLASS, and then zeroing the entire struct. Keeping in mind that WNDCLASS is an alias for either WNDCLASSA or WNDCLASSW, depending on how you're building the C++ program, and also keeping in mind that we're always going to be using the W versions of things, then the equivalent Rust would be something like this:

fn main () {
  let mut wc: WNDCLASSW = unsafe { core::mem::zeroed() };
}

We haven't even called the OS and we've already got unsafe stuff going on.

But... does this need to be unsafe that everyone thinks about? Is this the kind of unsafe action that we need to evaluate the correctness of every type we do it? No, not at all. It's always safe to make a default WNDCLASSW by zeroing the memory. We know that right now, and that doesn't change based on the situation. So we'll just give a Default impl to our type that does this for us.


#![allow(unused)]
fn main() {
impl Default for WNDCLASSW {
  #[inline]
  #[must_use]
  fn default() -> Self {
    unsafe { core::mem::zeroed() }
  }
}
}

In fact, this is going to be true for all the foreign C structs we declare. We'll just make a macro to handle this for us consistently. When you're making a lot of bindings by hand, consistency is king.


#![allow(unused)]
fn main() {
macro_rules! unsafe_impl_default_zeroed {
  ($t:ty) => {
    impl Default for $t {
      #[inline]
      #[must_use]
      fn default() -> Self {
        unsafe { core::mem::zeroed() }
      }
    }
  };
}
}

"Lokathor, why did you put unsafe in that macro name? Default isn't an unsafe trait." Good question. It's because the macro could be used improperly. The unsafe block around the call to zeroed tells the compiler "no, hush, it's fine, I checked." So if you were to use the macro to make a Default impl for a type that can't be safely zeroed, then you'd sure have a problem on your hand.

Any time a macro hides away some sort of unsafe thing, you should put unsafe in the name. It's a simple convention, but it keeps it obvious that the macro can go wrong if misused.

Now our rust can look like this:

fn main() {
  let mut wc = WNDCLASSW::default();
}

And that's so much nicer, at least to my eyes.

Writing a Window Procedure

The guide says

We'll examine the window procedure in detail later. For now, just treat this as a forward reference.

So, for now we'll just make a dummy window procedure that panics if it's actually called.


#![allow(unused)]
fn main() {
unsafe extern "system" fn dummy_window_procedure(
  hwnd: HWND, uMsg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  unimplemented!()
}
}

And we can start filling in the wc value:

fn main() {
  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = todo!();
  wc.lpszClassName = todo!();
}

Getting the HINSTANCE

This next part is a hair tricky to solve on your own.

What the tutorial wants us to do is pass the hInstance value that we were given at the start of the WinMain function. Except the problem is that we're not writing a Windows C++ program so we don't have a WinMain function at all. We're writing a Rust program, and the Rust program starts at fn main(), with no instance argument.

If we just ask the internet about "msdn get my instance" then there's not too much help. However, if we phrase it more like "msdn get my hinstance c++" then there's a lovely StackOverflow asking about this very situation. If we call GetModuleHandle(NULL) we can get the HINSTANCE of our exe.

Interestingly, one of the comments on the question also says that we can just plain pass NULL as our instance value and it'll be fine. However, the MSDN tutorial says to pass an HINSTANCE, and this pushes us to learn a bit and try a new thing, so we'll at least try the GetModuleHandle way first.

If we look up GetModuleHandle, we see that it has an A-form and W-form, since it takes a name, and the name is textual. We want to use GetModuleHandleW, as discussed.

If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).

Sounds good.

fn main() {
  let hInstance = GetModuleHandleW(core::ptr::null());

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  wc.lpszClassName = todo!();
}

Well, obviously this won't work, but let's check that error message for fun:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
error[E0425]: cannot find function, tuple struct or tuple variant `GetModuleHandleW` in this scope
  --> src\main.rs:18:19
   |
18 |   let hInstance = GetModuleHandleW(core::ptr::null());
   |                   ^^^^^^^^^^^^^^^^ not found in this scope

Okay, so we need to declare the function before we can use it. We do this with an external block.

An external block just declares the signature of a function, like this:


#![allow(unused)]
fn main() {
// EXAMPLE USAGE
extern ABI {
  fn NAME1(args) -> output;
  
  fn NAME2(args) -> output;

  // ...
}
}

The actual function is "external" to the program. To perform compilation, all the compiler really needs is the correct function signature. This allows it to perform type checking, and ensure the correct call ABI is used. Later on, the linker sorts it all out. If it turns out that a function can't be linked after all, you get a link error rather than a compile error.

But who tells the linker what to link with to find the external functions? Well, you can use a build script, or you can put it right on the extern block.


#![allow(unused)]
fn main() {
// EXAMPLE USAGE
#[link(name = "LibraryName")]
extern ABI {
  fn NAME1(args) -> output;
}
}

If the library is some sort of common system library that the linker will already know about, then it's perfectly fine to just use the attribute. In other cases, like if a library name varies by operating system, you might need the build script.

Where do we find GetModuleHandleW though? MSDN tells us right there on the page. If we look in the Requirements section we'll see:

DLL: Kernel32.dll

So in our Rust we have our declaration like this:


#![allow(unused)]
fn main() {
#[link(name = "Kernel32")]
extern "system" {
  /// [`GetModuleHandleW`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew)
  pub fn GetModuleHandleW(lpModuleName: LPCWSTR) -> HMODULE;
}
}

And now we can call GetModuleHandleW without error (if we put an unsafe block around the call):

fn main() {
  let hInstance = unsafe { GetModuleHandleW(core::ptr::null()) };

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  wc.lpszClassName = todo!();
}

Wide Strings

The last thing we need is one of those fancy LPCWSTR things. A "long pointer to a C-style wide string". Well a long pointer is just a pointer. And a wide string, to Windows, means a UTF-16 string. The only thing we haven't mentioned yet is the C-style thing.

There's two basic ways to handle strings.

  • "Null terminated", where the string is just a pointer, but it isn't allowed to contain 0. To determine the string's length you have to walk the string until you see a 0, and that's the end of the string.
  • "ptr+len", where the string is a pointer and a length, and the string can contain any value. To determine the length, you just check the length value.

Rust uses the ptr+len style for strings, as well as for slices in general. C and C++ use the null terminated style for strings.

It's not too difficult to convert a ptr+len string into a null terminated string, but it's also not entirely free. Pushing an extra 0 onto the end of the string is only cheap if there's spare capacity to do it. In the case of string literals, for example, you'd have to allocate a separate string, because the literal is kept in read-only memory.

The basic form of this is very simple code:


#![allow(unused)]
fn main() {
/// Turns a Rust string slice into a null-terminated utf-16 vector.
pub fn wide_null(s: &str) -> Vec<u16> {
  s.encode_utf16().chain(Some(0)).collect()
}
}

The .encode_utf16() makes the basic encoding iterator, then .chain(Some(0)) puts a 0 on the end of the iteration, and we just .collect() it into a totally normal Vec<u16>.

Long term, if we were using a lot of UTF-16, we might want to build a way to have these "C wide strings" computed as compile time and stored as literals. It lets the program allocate a little less as it performs its startup stuff. However, the code for that is a little hairy, and a bit of a side story compared to the current goal.

Soooo.... we can just write it like this, right?

fn main() {
  let hInstance = unsafe { GetModuleHandleW(core::ptr::null()) };

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  // BAD, WRONG, NO
  wc.lpszClassName = wide_null("Sample Window Class").as_ptr();
}

Ah, we can't do that! This is a classic beginner's mistake, but it must be avoided.

If we wrote it like that, the vec of utf-16 would get allocated, then we'd call as_ptr, assign that pointer to wc.lpszClassName, and then... the expression would end. And the vector would drop, and clean up, and deallocate the memory we wanted to point to. We'd have a dangling pointer, horrible. Maybe it'd even sometimes work anyway. The allocator might not re-use the memory right away, so it might still hold useful data for a while. It's still some nasty Undefined Behavior though.

Here's the correct way to do it:

fn main() {
  let hInstance = unsafe { GetModuleHandleW(core::ptr::null()) };
  let sample_window_class_wn = wide_null("Sample Window Class");

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  wc.lpszClassName = sample_window_class_wn.as_ptr();
}

This way, the sample_window_class_wn binding holds the vector live, and the pointer can be used for as long as that binding lasts. In this case, to the end of the main function.

Registering The Window Class

Okay, so our widow class request is all filled out, we just have to register it using RegisterClassW:

ATOM RegisterClassW(
  const WNDCLASSW *lpWndClass
);

And in Rust:


#![allow(unused)]
fn main() {
type ATOM = WORD;
type WORD = c_ushort;
type c_ushort = u16;
#[link(name = "User32")]
extern "system" {
  /// [`RegisterClassW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassw)
  pub fn RegisterClassW(lpWndClass: *const WNDCLASSW) -> ATOM;
}
}

It's a little weird sometimes to see that the const and * part are "around" the target type in C, and then both on the same side of the type in Rust, but that's genuinely the correct translation.

So now we can make the register call:

fn main() {
  let hInstance = unsafe { GetModuleHandleW(core::ptr::null()) };
  let sample_window_class_wn = wide_null("Sample Window Class");

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  wc.lpszClassName = sample_window_class_wn.as_ptr();

  unsafe { RegisterClassW(&wc) };
}

But we don't know if it worked or not. Almost any call to the operating system can fail. Cosmic rays and stuff. If we check the Return value part of the MSDN page it says:

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Hmm, let's check GetLastError, that sounds like a thing we'll want to use a lot.

yada yada... thead local error code... yada yada... some functions set an error code and then succeed... okay... "To obtain an error string for system error codes, use the FormatMessage function." Oof, we'd have a whole extra layer to dive into if we went down that path. "For a complete list of error codes provided by the operating system, see System Error Codes." Okay, well that's not too bad. For now, we can show an error code and then look it up by hand.


#![allow(unused)]
fn main() {
type DWORD = c_ulong;
type c_ulong = u32;
#[link(name = "Kernel32")]
extern "system" {
  /// [`GetLastError`](https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror)
  pub fn GetLastError() -> DWORD;
}
}

And now we have basic error checking / reporting:

fn main() {
  let hInstance = unsafe { GetModuleHandleW(core::ptr::null()) };
  let sample_window_class_wn = wide_null("Sample Window Class");

  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(dummy_window_procedure);
  wc.hInstance = hInstance;
  wc.lpszClassName = sample_window_class_wn.as_ptr();

  let atom = unsafe { RegisterClassW(&wc) };
  if atom == 0 {
    let last_error = unsafe { GetLastError() };
    panic!("Could not register the window class, error code: {}", last_error);
  }
}

Creating The Window

VS Code says I'm at like 4500 words already, and we haven't even made our Window yet.

To create a new instance of a window, call the CreateWindowEx function:

Okay, sure, that'll be nice and easy, no proble--

HWND CreateWindowExW(
  DWORD     dwExStyle,
  LPCWSTR   lpClassName,
  LPCWSTR   lpWindowName,
  DWORD     dwStyle,
  int       X,
  int       Y,
  int       nWidth,
  int       nHeight,
  HWND      hWndParent,
  HMENU     hMenu,
  HINSTANCE hInstance,
  LPVOID    lpParam
);

oof!

Okay, actually most of these we've seen before. This is getting easier the more we do.


#![allow(unused)]
fn main() {
type HMENU = HANDLE;
type LPVOID = *mut core::ffi::c_void;
#[link(name = "User32")]
extern "system" {
  /// [`CreateWindowExW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw)
  pub fn CreateWindowExW(
    dwExStyle: DWORD, lpClassName: LPCWSTR, lpWindowName: LPCWSTR,
    dwStyle: DWORD, X: c_int, Y: c_int, nWidth: c_int, nHeight: c_int,
    hWndParent: HWND, hMenu: HMENU, hInstance: HINSTANCE, lpParam: LPVOID,
  ) -> HWND;
}
}

CreateWindowEx returns a handle to the new window, or zero if the function fails. To show the window—that is, make the window visible —pass the window handle to the ShowWindow function

Hey, look, the MSDN docs are using some of that extended typography we mentioned before (those dashes).

Apparently we want our window creation to look something like this:

fn main() {
  // first register the class, as before

  let sample_window_name_wn = wide_null("Sample Window Name");
  let hwnd = unsafe {
    CreateWindowExW(
      0,
      sample_window_class_wn.as_ptr(),
      sample_window_name_wn.as_ptr(),
      WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT,
      CW_USEDEFAULT,
      CW_USEDEFAULT,
      CW_USEDEFAULT,
      core::ptr::null_mut(),
      core::ptr::null_mut(),
      hInstance,
      core::ptr::null_mut(),
    )
  };
  if hwnd.is_null() {
    panic!("Failed to create a window.");
  }
}

Now we just have to define WS_OVERLAPPEDWINDOW and CW_USEDEFAULT. These are defined in the header files as C macro values, which expand to literals. In Rust, we could define them as macros, but it'd be a little silly. We probably want to define them as const values instead.


#![allow(unused)]
fn main() {
const WS_OVERLAPPED: u32 = 0x00000000;
const WS_CAPTION: u32 = 0x00C00000;
const WS_SYSMENU: u32 = 0x00080000;
const WS_THICKFRAME: u32 = 0x00040000;
const WS_MINIMIZEBOX: u32 = 0x00020000;
const WS_MAXIMIZEBOX: u32 = 0x00010000;
const WS_OVERLAPPEDWINDOW: u32 = WS_OVERLAPPED
  | WS_CAPTION
  | WS_SYSMENU
  | WS_THICKFRAME
  | WS_MINIMIZEBOX
  | WS_MAXIMIZEBOX;
const CW_USEDEFAULT: c_int = 0x80000000_u32 as c_int;
}

There's more WS_ values you could define, but that's enough to start.

Oh, and heck, we probably want to just import null and null_mut since we'll be using them a lot.


#![allow(unused)]
fn main() {
use core::ptr::{null, null_mut};
}

For calling ShowWindow, we have a HWND already, but the show parameter is apparently another one of those WinMain arguments. Instead we'll just look at the list of what the ShowWindow docs say we can send, and I guess we can pick SW_SHOW.


#![allow(unused)]
fn main() {
const SW_SHOW: c_int = 5;
type BOOL = c_int;
#[link(name = "User32")]
extern "system" {
  /// [`ShowWindow`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow)
  pub fn ShowWindow(hWnd: HWND, nCmdShow: c_int) -> BOOL;
}
}

And we add a call to ShowWindow after we've made our window and checked for a successful creation.


#![allow(unused)]
fn main() {
  let _previously_visible = unsafe { ShowWindow(hwnd, SW_SHOW) };
}

Okay, if we run our program now we expect it to at least make the window, but then the window will close when we go off the end of the main function and the process ends. So, it'll probably flicker on screen really fast and then disappear, or something. Right?

Let's give it a try...

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target\debug\triangle-from-scratch.exe`
thread 'main' panicked at 'not implemented', src\main.rs:60:3
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\triangle-from-scratch.exe` (exit code: 0xc0000409, STATUS_STACK_BUFFER_OVERRUN)

Whoops! Haha, remember how we had that dummy window procedure? It's actually not supposed to panic and unwind the stack during the callback. Bad things end up happening. We just did it to fill in a little bit so the compiler would be cool.

Now that we're tying to turn on the program on for real (even for a second), we need a real window procedure. But we don't know how to write one yet. Never fear, there's a function called DefWindowProcW. It's the "Default Window Procedure", that you can use to handle any messages you don't want to handle. Right now, for us, that's all of them.

fn main() {
  // ...
  wc.lpfnWndProc = Some(DefWindowProcW);
  // ...
}

#[link(name = "User32")]
extern "system" {
  /// [`DefWindowProcW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw)
  pub fn DefWindowProcW(
    hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
  ) -> LRESULT;
}

And, finally, we can get a window to flicker on the screen!

Handling Window Messages

We're on to the next page of the tutorial!

Now we get to learn all about Window Messages

First we need to define this great MSG struct:

typedef struct tagMSG {
  HWND   hwnd;
  UINT   message;
  WPARAM wParam;
  LPARAM lParam;
  DWORD  time;
  POINT  pt;
  DWORD  lPrivate;
} MSG, *PMSG, *NPMSG, *LPMSG;

In Rust:


#![allow(unused)]
fn main() {
#[repr(C)]
pub struct MSG {
  hwnd: HWND,
  message: UINT,
  wParam: WPARAM,
  lParam: LPARAM,
  time: DWORD,
  pt: POINT,
  lPrivate: DWORD,
}
unsafe_impl_default_zeroed!(MSG);
}

Hey look, we have nearly all of that defined already.


#![allow(unused)]
fn main() {
type LONG = c_long;
type c_long = i32;
#[repr(C)]
pub struct POINT {
  x: LONG,
  y: LONG,
}
unsafe_impl_default_zeroed!(POINT);
}

And now we can get our window messages.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`GetMessageW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew)
  pub fn GetMessageW(
    lpMsg: LPMSG, hWnd: HWND, wMsgFilterMin: UINT, wMsgFilterMax: UINT,
  ) -> BOOL;
}
}

We have to get them in a loop of course, because we'll be getting a whole lot of them.

fn main() {
  // first open the window

  let mut msg = MSG::default();
  loop {
    let message_return = unsafe { GetMessageW(&mut msg, null_mut(), 0, 0) };
    if message_return == 0 {
      break;
    } else if message_return == -1 {
      let last_error = unsafe { GetLastError() };
      panic!("Error with `GetMessageW`, error code: {}", last_error);
    }
  }
}

Except we're missing TranslateMessage and DispatchMessageW.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`TranslateMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-translatemessage)
  pub fn TranslateMessage(lpMsg: *const MSG) -> BOOL;

  /// [`DispatchMessageW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-dispatchmessagew)
  pub fn DispatchMessageW(lpMsg: *const MSG) -> LRESULT;
}
}

Okay so then we put those into our loop, if there's no problem with getting the message:


#![allow(unused)]
fn main() {
  loop {
    let message_return = unsafe { GetMessageW(&mut msg, null_mut(), 0, 0) };
    if message_return == 0 {
      break;
    } else if message_return == -1 {
      let last_error = unsafe { GetLastError() };
      panic!("Error with `GetMessageW`, error code: {}", last_error);
    } else {
      unsafe {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
      }
    }
  }
}

There's a lot of good info on the page about window messages, but that's all we have to do here in terms of our code.

By now, our program can open a window. However, we also see an eternally spinning mouse once it's open. We also don't see our program close when we close the window. It just continues to spin in the loop, and we have to exit it by pressing Ctrl+C in the command line.

Writing The Window Procedure

Next up is Writing the Window Procedure.

That default window procedure we've been using so far is fine for most events. Usually it just ignores every event. However, a few event types can't just be ignored. One of them is that window closing situation. Another is that thing with the mouse cursor.

First let's do the window closing and cleanup. If we look at MSDN page for the WM_CLOSE message, we can see that we'll need to be able to use DestroyWindow and PostQuitMessage. We also need to respond to WM_DESTROY as well.


#![allow(unused)]
fn main() {
pub const WM_CLOSE: u32 = 0x0010;
pub const WM_DESTROY: u32 = 0x0002;

#[link(name = "User32")]
extern "system" {
  /// [`DestroyWindow`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow)
  pub fn DestroyWindow(hWnd: HWND) -> BOOL;

  /// [`PostQuitMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage)
  pub fn PostQuitMessage(nExitCode: c_int);
}
}

And we have to write our own procedure. This time, no panics.


#![allow(unused)]
fn main() {
pub unsafe extern "system" fn window_procedure(
  hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  0
}
}

There's a few different ways we can arrange the branching here, and it comes down to taste in the end, but most of the messages should return 0 when you've processed them. We'll assume that 0 is the "normal" response and build our setup around that.


#![allow(unused)]
fn main() {
pub unsafe extern "system" fn window_procedure(
  hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  match Msg {
    WM_CLOSE => DestroyWindow(hWnd),
    WM_DESTROY => PostQuitMessage(0),
    _ => return DefWindowProcW(hWnd, Msg, wParam, lParam),
  }
  0
}
}

One little problem here is that DestroyWindow and PostQuitMessage have different return types. Even though we're ignoring the output of DestroyWindow, it's a type error to have it like this. We can suppress the output of DestroyWindow by putting it in a block and having a ; after it.


#![allow(unused)]
fn main() {
pub unsafe extern "system" fn window_procedure(
  hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  match Msg {
    WM_CLOSE => {
      DestroyWindow(hWnd);
    }
    WM_DESTROY => PostQuitMessage(0),
    _ => return DefWindowProcW(hWnd, Msg, wParam, lParam),
  }
  0
}
}

Ehhhhhh, I'm not sure if I'm a fan of rustfmt making it look like that.


#![allow(unused)]
fn main() {
pub unsafe extern "system" fn window_procedure(
  hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  match Msg {
    WM_CLOSE => drop(DestroyWindow(hWnd)),
    WM_DESTROY => PostQuitMessage(0),
    _ => return DefWindowProcW(hWnd, Msg, wParam, lParam),
  }
  0
}
}

Oh, yeah, that's the good stuff. We can use drop to throw away the i32 value, so then we don't need the ; and braces, so rustfmt keeps it on a single line. I am all about that compact code stuff.

Now we can open the window and click for it to close and the program actually terminates.

Fixing The Cursor (maybe?)

The mouse cursor is still kinda funky. It gets kinda... stuck with different icons. If you move the mouse into the window area from different sides, the little "adjust window size" cursors don't change to the normal cursor once the mouse is in the middle of the window. That's mostly our fault, we left the cursor for our Window Class as null.

Instead, if we use LoadCursorW we can assign a cursor to our window class, and then the default window procedure will set the cursor to be the right image at the right time.

We're supposed to call it with something like:

fn main() {
  // ...
  wc.hCursor = unsafe { LoadCursorW(hInstance, IDC_ARROW) };
  // ...
}

And the extern function is easy to do:


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`LoadCursorW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw)
  pub fn LoadCursorW(hInstance: HINSTANCE, lpCursorName: LPCWSTR) -> HCURSOR;
}
}

But how do we make that IDC_ARROW thing? In the docs for LoadCursorW they're all listed as MAKEINTRESOURCE(number). Okay so we look up MAKEINTRESOURCEW and... it's a C macro. Blast. Okay so we can't link to it and call it, instead we'll have to grep the windows includes to see what's happening.

C:\Program Files (x86)\Windows Kits\10\Include>rg "#define MAKEINTRESOURCE"
10.0.16299.0\um\WinUser.h
215:#define MAKEINTRESOURCEA(i) ((LPSTR)((ULONG_PTR)((WORD)(i))))
216:#define MAKEINTRESOURCEW(i) ((LPWSTR)((ULONG_PTR)((WORD)(i))))
218:#define MAKEINTRESOURCE  MAKEINTRESOURCEW
220:#define MAKEINTRESOURCE  MAKEINTRESOURCEA

10.0.16299.0\shared\ks.h
4464:#define MAKEINTRESOURCE( res ) ((ULONG_PTR) (USHORT) res)

This is a #define, which technically isn't C code. It's a declaration for a thing called the "C Pre-Processor", which is a tool that runs over a source file before the text gets to the compiler itself. In the case of a #define, the general form is #define original replacement. Any occurrence of the original is replaced (textually) with replacement. So #define MAKEINTRESOURCE MAKEINTRESOURCEW makes all occurrences of MAKEINTRESOURCE become MAKEINTRESOURCEW, before the compiler even sees the source code. Since there are two different #define statements for MAKEINTRESOURCE we can assume that they're conditional, and a given program would get one or the other during compilation. If you go investigate WinUser.h you'll see that this is the case, but I don't really want to quote source code blocks from Microsoft files, because I'm not clear on how big of a quote block would be "fair use".

Anyway, if there's input arguments on the original then they can be written to accept an expression. And then the expansion puts the expression in somewhere. That's how MAKEINTRESOURCEW(i) works, anything inside the parentheses becomes i in the replacement expression. MAKEINTRESOURCEW(2+2) would become ((LPWSTR)((ULONG_PTR)((WORD)(2+2)))).

Finally, if there's a backslash at the end of a #define line it will continue into the next line. That's not the case here, but more complicated defines can end up using backslashes, so you should just be aware if you're trying to read a lot of C code.

In C these are called "macros", but because they're so different from how a macro works in Rust, I'll try to just call them a #define.

Back to examining MAKEINTRESOURCE itself. The input value is cast to a WORD, then cast directly to a ULONG_PTR, then cast directly to a string pointer (either ansi or wide). That's not too hard for us to do.

Since this was done as a C-style macro, you might think that we'd want to do a Rust-style macro too. However, I think we might want to use a const fn instead. I just like having the type checking when possible.


#![allow(unused)]
fn main() {
type LPWSTR = *mut WCHAR;
type ULONG_PTR = usize;
/// [`MAKEINTRESOURCEW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-makeintresourcew)
pub const fn MAKEINTRESOURCEW(i: WORD) -> LPWSTR {
  i as ULONG_PTR as LPWSTR
}
const IDC_ARROW: LPCWSTR = MAKEINTRESOURCEW(32512);
}

Did that fix it? Huh. Nope.

Sometimes you're really sure that you know what's wrong, it's just so obvious, but you didn't know at all, and you basically wasted your time with some dead end. This is frustrating, but it's okay. We all make mistakes.

In some web searches about this problem, I've heard that the mouse cursor sometimes is heuristic in Windows, and once Windows thinks that your app is behaving properly, it'll make the mouse cursor so the right thing for you. I don't have any idea if this is true or not. If it is true, then maybe once we add more to our program the mouse cursor will start working right. Let's move on, we can come back to this later.

Painting The Window

The tutorial wants to tell us about Painting The Window next.

So we have to accept a WM_PAINT message:


#![allow(unused)]
fn main() {
const WM_PAINT: u32 = 0x000F;
}

And then do a little dance with a PAINTSTRUCT, as well as the additional types it depends on:


#![allow(unused)]
fn main() {
#[repr(C)]
pub struct PAINTSTRUCT {
  hdc: HDC,
  fErase: BOOL,
  rcPaint: RECT,
  fRestore: BOOL,
  fIncUpdate: BOOL,
  rgbReserved: [BYTE; 32],
}
unsafe_impl_default_zeroed!(PAINTSTRUCT);
type HDC = HANDLE;
type BYTE = u8;
#[repr(C)]
pub struct RECT {
  left: LONG,
  top: LONG,
  right: LONG,
  bottom: LONG,
}
unsafe_impl_default_zeroed!(RECT);
}

This is all becoming routine by now, I hope.

They want us to use BeginPaint, then FillRect on the whole canvas, then EndPaint. Sounds easy enough to do.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`BeginPaint`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-beginpaint)
  pub fn BeginPaint(hWnd: HWND, lpPaint: LPPAINTSTRUCT) -> HDC;

  /// [`FillRect`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-fillrect)
  pub fn FillRect(hDC: HDC, lprc: *const RECT, hbr: HBRUSH) -> c_int;

  /// [`EndPaint`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-endpaint)
  pub fn EndPaint(hWnd: HWND, lpPaint: *const PAINTSTRUCT) -> BOOL;
}
const COLOR_WINDOW: u32 = 5;
}

The COLOR_WINDOW constant I had to look up in the headers.

Now we adjust the window procedure a bit to do the painting:


#![allow(unused)]
fn main() {
pub unsafe extern "system" fn window_procedure(
  hWnd: HWND, Msg: UINT, wParam: WPARAM, lParam: LPARAM,
) -> LRESULT {
  match Msg {
    WM_CLOSE => drop(DestroyWindow(hWnd)),
    WM_DESTROY => PostQuitMessage(0),
    WM_PAINT => {
      let mut ps = PAINTSTRUCT::default();
      let hdc = BeginPaint(hWnd, &mut ps);
      let _success = FillRect(hdc, &ps.rcPaint, (COLOR_WINDOW + 1) as HBRUSH);
      EndPaint(hWnd, &ps);
    }
    _ => return DefWindowProcW(hWnd, Msg, wParam, lParam),
  }
  0
}
}

Window looks the same as before, but if we fiddle with the brush value we can see it'll draw using other colors. Doesn't seem to fix the mouse though.

Closing The Window

The tutorial page about Closing The Window has a fun part where we can open a message box. I like the occasional message box, let's do that.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`MessageBoxW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxw)
  pub fn MessageBoxW(
    hWnd: HWND, lpText: LPCWSTR, lpCaption: LPCWSTR, uType: UINT,
  ) -> c_int;
}
const MB_OKCANCEL: u32 = 1;
const IDOK: c_int = 1;
}

So here's a fun question, how do we decide when a constant should be u32 or c_int or whatever type of int? The correct answer is that there's no correct answer. I just pick based on how I see the API using it most of the time. That is, if it's a const that gets compared to a return from a function, we use the function's return type. If it's a const we pass to a function, we use the function's argument type. Sometimes a value will be used as more than one type of number, then you'll have to just pick one. In C the number types can just automatically convert, so they don't really care. In Rust, that's not the case, so I just try to pick a default type for the value. So that most of the time I can write MY_CONST and not MY_CONST as _.

Managing Application State

Ah, we're back to a slightly tricky part of things. In Managing Application State we get some notions thrown around like "use just global variables until it gets too complicated!" Well, no thanks MSDN. I'll go directly to the stage where there's no global variables.

First, we need to be ready to handle WM_NCCREATE and WM_CREATE:


#![allow(unused)]
fn main() {
const WM_NCCREATE: u32 = 0x0081;
const WM_CREATE: u32 = 0x0001;
}

And we check for them in our window procedure:


#![allow(unused)]
fn main() {
// in the window_procedure
  match Msg {
    WM_NCCREATE => {
      println!("NC Create");
      return 1;
    }
    WM_CREATE => println!("Create"),
}

Let's see those messages print out...

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target\debug\triangle-from-scratch.exe`
NC Create
thread 'main' panicked at 'Failed to create a window.', src\main.rs:53:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\triangle-from-scratch.exe` (exit code: 101)

Nani!? Something already went wrong. Better check the full docs for WM_NCCREATE. Ah, see, it's right there.

Return Value: If an application processes this message, it should return TRUE to continue creation of the window. If the application returns FALSE, the CreateWindow or CreateWindowEx function will return a NULL handle.

Okay, so far all of our messages have asked us to just always return 0 when the message was handled, and this is the first message we've been handling that we had to decide to return 0 or not. Well, right now our window creation should always proceed, so here we go:


#![allow(unused)]
fn main() {
// in the window_procedure
WM_NCCREATE => {
  println!("NC Create");
  return 1;
}
}

And give this a test now:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s
     Running `target\debug\triangle-from-scratch.exe`
NC Create
Create

Naisu!

Hey, better check on WM_CREATE to see if it has any return stuff we just got right on accident:

Return value: If an application processes this message, it should return zero to continue creation of the window. If the application returns –1, the window is destroyed and the CreateWindowEx or CreateWindow function returns a NULL handle.

Ah, yeah, we were getting it right sorta on accident. Gotta always read those docs.

Okay now we continue the tutorial:

The last parameter of CreateWindowEx is a pointer of type void*. You can pass any pointer value that you want in this parameter. When the window procedure handles the WM_NCCREATE or WM_CREATE message, it can extract this value from the message data.

Right, so, we have to have a void pointer to pass to the message. Uh, just to pick something, let's pass our message a pointer to the number 5.


#![allow(unused)]
fn main() {
// in main
let lparam: *mut i32 = Box::leak(Box::new(5_i32));
let hwnd = unsafe {
  CreateWindowExW(
    0,
    sample_window_class_wn.as_ptr(),
    sample_window_name_wn.as_ptr(),
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    null_mut(),
    null_mut(),
    hInstance,
    lparam.cast(),
  )
};
}

So we're making a boxed i32, then we leak the box because we don't want Rust to drop this box based on scope. Instead, we'll clean up the box as part of the window destruction.

When you receive the WM_NCCREATE and WM_CREATE messages, the lParam parameter of each message is a pointer to a CREATESTRUCT structure.

The CREATESTRUCT type has A and W forms. Since we're using CreateWindowExW, we'll assume that we use CREATESTRUCTW here.


#![allow(unused)]
fn main() {
#[repr(C)]
pub struct CREATESTRUCTW {
  lpCreateParams: LPVOID,
  hInstance: HINSTANCE,
  hMenu: HMENU,
  hwndParent: HWND,
  cy: c_int,
  cx: c_int,
  y: c_int,
  x: c_int,
  style: LONG,
  lpszName: LPCWSTR,
  lpszClass: LPCWSTR,
  dwExStyle: DWORD,
}
unsafe_impl_default_zeroed!(CREATESTRUCTW);
}

Now we can get out the boxed pointer thing from the create struct:


#![allow(unused)]
fn main() {
WM_NCCREATE => {
  println!("NC Create");
  let createstruct: *mut CREATESTRUCTW = lParam as *mut _;
  if createstruct.is_null() {
    return 0;
  }
  let boxed_i32_ptr: *mut i32 = (*createstruct).lpCreateParams.cast();
  return 1;
}
}

And then we use SetWindowLongPtrW to connect this create struct pointer to the window itself.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`SetWindowLongPtrW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptrw)
  pub fn SetWindowLongPtrW(
    hWnd: HWND, nIndex: c_int, dwNewLong: LONG_PTR,
  ) -> LONG_PTR;
}
const GWLP_USERDATA: c_int = -21;
}

And it's fairly simple to call, but we have to put a manual cast to LONG_PTR in:


#![allow(unused)]
fn main() {
WM_NCCREATE => {
  println!("NC Create");
  let createstruct: *mut CREATESTRUCTW = lParam as *mut _;
  if createstruct.is_null() {
    return 0;
  }
  let boxed_i32_ptr = (*createstruct).lpCreateParams;
  SetWindowLongPtrW(hWnd, GWLP_USERDATA, boxed_i32_ptr as LONG_PTR);
  return 1;
}
}

And then we can use GetWindowLongPtrW to get our windows's custom user data:


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`GetWindowLongPtrW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw)
  pub fn GetWindowLongPtrW(hWnd: HWND, nIndex: c_int) -> LONG_PTR;
}
}

Now, uh, we'll print out the current value or something. I guess, each time we go to WM_PAINT we'll print the value and add 1 to it. Just any old thing to see that it's working.


#![allow(unused)]
fn main() {
WM_PAINT => {
  let ptr = GetWindowLongPtrW(hWnd, GWLP_USERDATA) as *mut i32;
  println!("Current ptr: {}", *ptr);
  *ptr += 1;
  let mut ps = PAINTSTRUCT::default();
  let hdc = BeginPaint(hWnd, &mut ps);
  let _success = FillRect(hdc, &ps.rcPaint, (COLOR_WINDOW + 1) as HBRUSH);
  EndPaint(hWnd, &ps);
}
}

That'll print 5, and then if you force a bunch of paint messages you can see it count up. The easiest way to do that is to adjust the window's size so that it's small, then drag it to be bigger. Each time the window's size expands it triggers new paint messages.

Of course, we also can't forget that cleanup code we promised. The way we do the cleanup is to just turn the raw pointer back into a Box<i32>. The drop code for the Box type will handle the rest for us. Of course, we should only do this right as the window is being destroyed.


#![allow(unused)]
fn main() {
WM_DESTROY => {
  let ptr = GetWindowLongPtrW(hWnd, GWLP_USERDATA) as *mut i32;
  Box::from_raw(ptr);
  println!("Cleaned up the box.");
  PostQuitMessage(0);
}
}

And finally, I think we're done!

Hey, What About The Triangle?

Well, there's several ways to draw a triangle in windows. You can use DirectX, OpenGL, Vulkan, probably some other ways I don't even know about. This lesson is going to stop at just the window creation part. Then, each other lesson on a particular Windows drawing API can assume you've read this as a baseline level of understanding.

Fixing That Stupid Cursor

Before I finished this, I really wanted to figure out what was going on with that cursor.

So what I did was, first we want to handle the WM_SETCURSOR event, and then in the event we call the SetCursor function.


#![allow(unused)]
fn main() {
// ...
    WM_SETCURSOR => {
      let hInstance = GetModuleHandleW(null());
      let cursor = LoadCursorW(hInstance, IDC_ARROW);
      let _old_cursor = SetCursor(cursor);
      //
      return 1;
    }
// ...

#[link(name = "User32")]
extern "system" {
  /// [`SetCursor`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setcursor)
  pub fn SetCursor(hCursor: HCURSOR) -> HCURSOR;
}
const WM_SETCURSOR: u32 = 0x0020;
}

Alright, so, what happens if you do that? Well, the cursor disappears entirely. What? Why? Well, let's check the docs for SetCursor. Hm, hmm, hmmmm, yeah.

If this parameter is NULL, the cursor is removed from the screen.

Okay, so we must be getting null for cursor. So LoadCursorW(hInstance, IDC_ARROW) is returning null... and that's how we're setting wc.hCursor during startup! So we must have been setting null this whole time. What fools we've been.

Okay let's check out the MSDN guide on Setting the Cursor Image. They've got some sample code:

hCursor = LoadCursor(NULL, cursor);
SetCursor(hCursor);

Oh. Huh. So, you're supposed to pass null to the load call? I guess that makes sense. I mean we had an HINSTANCE sitting around and we just used it, but whe you think about it, our own executable file probably doesn't contain the IDC_ARROW cursor.

So if we delete the cursor handling code from the window procedure, and just adjust the startup code to be correct:


#![allow(unused)]
fn main() {
let mut wc = WNDCLASSW::default();
wc.lpfnWndProc = Some(window_procedure);
wc.hInstance = hInstance;
wc.lpszClassName = sample_window_class_wn.as_ptr();
wc.hCursor = unsafe { LoadCursorW(null_mut(), IDC_ARROW) };
}

Well now our cursor works just fine!

The day is saved!

windows_subsystem

A last note before we go. The Windows operating system has two "subsystems" that a program can target: "console" and "windows". The only difference to us Rust folks is if the program gets a console allocated for it automatically by the system.

  • A console subsystem program gets a console allocated for it. If you run it from an existing console it'll use that one, but if you run it via other means it'll open a new console of its own while it's running.
  • A windows subsystem program does not get a console allocated for it.

If you try to use standard output or standard error, such as with println! and eprintln!, and there's no currently allocated console, then the output goes nowhere.

By default, a Rust program is a console subsystem program. This is fine, because it means that we can print debug messages. However, it also means that if we give our program to a friend and they run it, maybe by double clicking the executable in windows explorer, then they'll see the window that we want open up and also they'll see a mysterious console open in the background. If you really really mean for end users to see that console appear then that's fine, but most people don't expect an extra console to appear when the run a Windows program. It feels a little janky for this debug thing to show up along side the "real" program's window.

This is easy enough to fix. If we use the windows_subsystem attribute on our program we can pick the subsystem we want the program to use.


#![allow(unused)]
fn main() {
// at the top of the file
#![windows_subsystem = "windows"]
}

However, if we use that attribute directly then even debugging builds don't get a console.

Instead, I like to use cfg_attr to set the windows_subsystem value any time we're not using debug assertions. That's not exactly the same as "whenever it's a release build", but close enough.


#![allow(unused)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
}

And now we can send a release build of the program to our friends and have it act like they expect a normal Win32 program to act.

Win32 Window Cleanup

During our introduction to Win32 I said that we'd just write all our code into main.rs, and then we could sort it into the library later.

Well, later is now.

New Files

This isn't very hard. Cargo follows a "convention over configuration" style, so as long as we do what it expects we won't even have to change Cargo.toml or anything.

First we copy src/main.rs into examples/win32_window_standalone.rs, so that we can keep our nice small version that's all in one file. Over time our library will build up, and later lessons will refer to things we've put in our library already. However, this first example can be understood in a single file, without a person having to know what we've put into our library. I think that's pretty valuable, so we will preserve this first draft for later viewing.

Then we make src/lib.rs. This holds the top level items of the library. Within the crate's module hierarchy, lib.rs is a module is above the other modules of the crate. Within the filesystem on disk, lib.rs is a file beside the other files that make up the crate. With all other modules, the default filesystem location for a module matches the module's logical crate location. With lib.rs it's just slightly magical, so it has a different default filesystem location. Sometimes people find this one exception confusing, so I'm trying to be extra clear about what's going on regarding this point.

Next, since this is all Win32 specific stuff we'll be putting in the library right now, which obviously doesn't work on all targets, we'll make a src/win32.rs file, and then declare it as a conditional module within the library.


#![allow(unused)]
fn main() {
// in lib.rs

#[cfg(windows)]
pub mod win32;
}

Now, if we're on windows, we'll be able to use our nice win32 module.

Put The Declarations In The Library

Okay first we put every single type, struct, const, and extern declaration into win32.rs. We also have to make sure that all the fields and types are marked as pub.

Then we put a use triangle_from_scratch::win32::*; in our program.

The main.rs should only be left with main and window_procedure, but the program should build and run exactly as before.

Also, I'm going to remove the allow attributes at the top of the main.rs, and then convert the file to standard Rust naming for all the variables. This doesn't make the program do better things, but it helps people read it.

We Got Too Much Unsafe

All this unsafe code isn't great. Having unsafe code around means that if we aren't careful we don't get "just" a wrong output or an unexpected panic, instead we get some sort of Undefined Behavior (UB). UB might do what you expected, or it might segfault, or it might be a security vulnerability. I'd rather not have a security vulnerability in my program, so I'd like to reduce the amount of unsafe code in the program as much as I can.

/rant start

Let's be very plain: You cannot fully eliminate unsafe.

Fundamentally, interacting with the outside world is unsafe. If your program doesn't have any unsafe anywhere in the call stack, then it's not actually interacting with the world at all, and that's a pretty useless program to be running.

Operating systems and device drivers aren't designed to be free of UB. They are designed for you to pay attention, and ask for the right thing at the right time. We have to run unsafe code to get anything useful done.

However, unsafe code is not automatically the end of everything. It's code that can go wrong, but that doesn't means it must go wrong.

Fire can burn down your house, but you also need it to forge metal.

/rant end

Our job, every time we use an unsafe block, is to make sure, either with compile time checks or runtime checks, that we don't call any unsafe functions improperly.

It's "that simple". You know, "just" get good.

Every time we have an unsafe block, that block needs to be audited for correctness. The less unsafe blocks we have, the less we need to audit.

Our strategy is that we want to put our unsafe blocks inside of safe functions, and then the safe function performs whatever checks it needs to before actually making the unsafe function call. That might mean a runtime check, or the type system might even allow for static correctness to be known without a runtime check. Either way, we get it right once in the safe wrapper function, and then all other code only calls the safe function from there on. Then we only have to audit the unsafe block in one location, not everywhere all over the codebase.

I know, "put it in a function" is a very basic concept. You could have thought of that too, I'm sure. Still, sometimes it helps just to put the plan into words, even if it seems obvious.

Safe Wrapping GetModuleHandleW

Alright so what can we actually make into safe functions?

Well the first thing the program does is call GetModuleHandleW(null()). Is that legal? I mean we can at least say that it seems intentional, that doesn't mean it's correct. Let's checks the docs for GetModuleHandleW.

lpModuleName: ... If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).

Okay, not only are we allowed to pass null, but if we do then we get a special sort of default-ish return value.

In terms of an interface for a wrapper function for this, I think it'd be a little awkward to try and accept both null and non-null arguments and have it be ergonomic and stuff. Thankfully, we don't actually have to do that. We're just using the null argument style, and if we use the non-null argument variant later we can just make it a totally separate function later. So let's write a function for calling GetModuleHandleW(null()):


#![allow(unused)]
fn main() {
/// Returns a handle to the file used to create the calling process (.exe file)
///
/// See [`GetModuleHandleW`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew)
pub fn get_process_handle() -> HMODULE {
  // Safety: as per the MSDN docs.
  unsafe { GetModuleHandleW(null()) }
}
}

And now we can call that at the start of our fn main() in main.rs:

fn main() {
  let hinstance = get_process_handle();
  // ...

No unsafe block! One less thing to audit!

Safe Wrapping LoadCursorW

So we go down a bit, the next unsafe function is LoadCursorW. That one gave us some trouble for quite a while. I was passing the wrong argument in the first position, and then getting a null back without realizing it. It wasn't actually UB, it was an allowed function call to make, but I should have been checking the function's output and handling the error. Forgetting to handle the error case is what was causing my downfall.

Let's have a look at LoadCursorW.

We see that the HINSTANCE we pass has to be an executable file with a cursor in it. That's a thing, did you know that? You can put cursors and icons inside of executables and then programs can pick them out and use them. I knew you could do that with icons for desktop shortcuts, but I guess it works with cursors too. Neat.

Right now the program says LoadCursorW(null_mut(), IDC_ARROW), so can we pass null? It doesn't say anything about null in the description for hInstance, but lower down in the description for lpCursorName it says if you pass an IDC_ value from the list as the lpCursorName, then in that case you should set hInstance to null.

Like with how we wrapped GetModuleHandleW, we don't need to make a single wrapper function that handles every possible case that LoadCursorW does. Here, lets just make a function for loading the IDC_ cursors. If we want to load cursors out of non-null instances later on that can be a separate function.

And let's not forget to check that Return Value error case information:

If the function succeeds, the return value is the handle to the newly loaded cursor. If the function fails, the return value is NULL. To get extended error information, call GetLastError.

Simple enough. What do the Remarks have to say? Ah, this part sounds important:

This function returns a valid cursor handle only if the lpCursorName parameter is a pointer to a cursor resource. If lpCursorName is a pointer to any type of resource other than a cursor (such as an icon), the return value is not NULL, even though it is not a valid cursor handle.

Yikes, so we want to be very sure that we're passing a value from that list of allowed values.

Okay, so what if we make an enum of what you're allowed to pass in. On the Rust side, you can only have an enum of a real variant. Constructing an improper enum is already UB, so we don't even have to think about that case. If we define our enum properly then we know people will only be allowed to pass correct values.

And since things can error, let's use a Result type for the output. For now, the error will just be the () type. We'll come back here once we've looked at GetLastError, and made ourselves a useful error code type.


#![allow(unused)]
fn main() {
/// The predefined cursor styles.
pub enum IDCursor {
  /// Standard arrow and small hourglass
  AppStarting = 32650,
  /// Standard arrow
  Arrow = 32512,
  /// Crosshair
  Cross = 32515,
  /// Hand
  Hand = 32649,
  /// Arrow and question mark
  Help = 32651,
  /// I-beam
  IBeam = 32513,
  /// Slashed circle
  No = 32648,
  /// Four-pointed arrow pointing north, south, east, and west
  SizeAll = 32646,
  /// Double-pointed arrow pointing northeast and southwest
  SizeNeSw = 32643,
  /// Double-pointed arrow pointing north and south
  SizeNS = 32645,
  /// Double-pointed arrow pointing northwest and southeast
  SizeNwSe = 32642,
  /// Double-pointed arrow pointing west and east
  SizeWE = 32644,
  /// Vertical arrow
  UpArrow = 32516,
  /// Hourglass
  Wait = 32514,
}

/// Load one of the predefined cursors.
///
/// See [`LoadCursorW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-loadcursorw)
pub fn load_predefined_cursor(cursor: IDCursor) -> Result<HCURSOR, ()> {
  // Safety: The enum only allows values from the approved list. See MSDN.
  let hcursor =
    unsafe { LoadCursorW(null_mut(), MAKEINTRESOURCEW(cursor as WORD)) };
  if hcursor.is_null() {
    Err(())
  } else {
    Ok(hcursor)
  }
}
}

That's a lot of variants, but the wrapper function is still very simple. The tag values of the enum are each set to the MAKEINTRESOURCEW input shown in the documentation. When an enum is passed in, cursor as WORD will give us the tag value. We pass that value to MAKEINTRESOURCEW, then it goes off to LoadCursorW.

Also, remember that MAKEINTRESOURCEW is just some type casting stuff, it's not actually making any resources we have to free up later.

Let's check our fn main with this update:


#![allow(unused)]
fn main() {
  let mut wc = WNDCLASSW::default();
  wc.lpfnWndProc = Some(window_procedure);
  wc.hInstance = hinstance;
  wc.lpszClassName = sample_window_class_wn.as_ptr();
  wc.hCursor = load_predefined_cursor(IDCursor::Arrow).unwrap();
}

Ah ha! Now it's clear that we're going for a predefined cursor, and it's clear that the call could fail. Of course, using unwrap isn't a very robust way to solve problems. It's absolutely not allowed in a good library (always pass the error back up!), but in a binary it's "sorta okay, I guess", particularly since this is a demo.

Partial Wrapping RegisterClassW

Partial wrapping? I can hear you asking.

Yeah, you can't always make it safe. But you can at least almost always make it better typed.

If we have a look at the docs of our next unsafe function call, RegisterClassW, It says "You must fill the structure with the appropriate class attributes before passing it to the function."

But the WNDCLASSW type is full of pointers to strings and stuff. There's like three things that aren't pointers, and then all the rest is a pile of pointers. We don't have any easy way to track the validity of all the fields. I'm sure it's possible to do something here to make sure that all fields are valid all the time, but I'm also sure that the amount of effort that it would take would exceed the effort to just use an unsafe block and audit that code every so often.

So we're going to make a wrapper function, but we'll leave it as an unsafe function. Even then, we can give a better input and output type.


#![allow(unused)]
fn main() {
/// Registers a window class struct.
///
/// ## Safety
///
/// All pointer fields of the struct must be valid.
///
/// See [`RegisterClassW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerclassw)
pub unsafe fn register_class(window_class: &WNDCLASSW) -> Result<ATOM, ()> {
  let atom = RegisterClassW(window_class);
  if atom == 0 {
    Err(())
  } else {
    Ok(atom)
  }
}
}

Okay, and then the usage code:


#![allow(unused)]
fn main() {
  let atom = unsafe { register_class(&wc) }.unwrap_or_else(|()| {
    let last_error = unsafe { GetLastError() };
    panic!("Could not register the window class, error code: {}", last_error);
  });
}

Hmm. At first glance, things didn't improve as much as we might have wanted. Ah, but here's an interesting thing. Now atom is marked as a totally unused variable. We can't even forget to check for an error any more, someone else already did that for us.

Still, the error case is very wonky. That needs a fix.

Safe Wrapping GetLastError

I'm pretty sure I remember how GetLastError worked. It was super simple, right? Yeah, it just gives you the thread-local last-error value.


#![allow(unused)]
fn main() {
/// Gets the thread-local last-error code value.
///
/// See [`GetLastError`](https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror)
pub fn get_last_error() -> DWORD {
  unsafe { GetLastError() }
}
}

Done. Right?

Naw, of course not. Keep reading.

To obtain an error string for system error codes, use the FormatMessage function. For a complete list of error codes provided by the operating system, see System Error Codes.

So we want to be able to print out the error string. Really, the least we can do for our users. Let's just have a quick look at FormatMessage and... oh... oh my... You see that function signature? There's flags, there's pointers, there's even a va_list thing we don't know about. Oof.

A Newtype For The Error Code

Okay, okay. When there's a lot to do, one step at a time is usually the best way to do it.


#![allow(unused)]
fn main() {
#[repr(transparent)]
pub struct Win32Error(pub DWORD);
}

But, unlike the other types so far, this type is really intended to be shown to people. For this, there's two main traits that Rust supports:

  • Debug is for when you want to show the value to a Rust programmer.
  • Display is for when you want to show the value to the general public.

So for our Debug impl, it can just show "Win32Error(12)" or similar. This is exactly what a derived Debug impl will do, so we'll use the derive:


#![allow(unused)]
fn main() {
#[derive(Debug)]
#[repr(transparent)]
pub struct Win32Error(pub DWORD);
}

For Display we can't derive an implementation. I don't mean that we shouldn't derive an implementation, but that we literally cannot. The standard library literally doesn't offer a derive for the Display trait. That's because the standard library is managed by people who are very silly. They have a silly concern that a derived Display impl "might" not show the right sort of info. Instead of saying "if the derive doesn't do the right thing for you, write the impl by hand", they just completely refuse to offer a derive at all. Like I said, completely silly.

But we won't dwell on that too much, because even if the derive was there, we wouldn't be able to use it in this case.

Instead... we get to use everyone's favorite function.... FormatMessageW.


#![allow(unused)]
fn main() {
impl core::fmt::Display for Win32Error {
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    todo!("call FormatMessageW")
  }
}
}

First we define the extern we need:


#![allow(unused)]
fn main() {
pub type LPCVOID = *const core::ffi::c_void;
pub type va_list = *mut c_char;
pub type c_char = i8;
#[link(name = "Kernel32")]
extern "system" {
  /// [`FormatMessageW`](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessagew)
  pub fn FormatMessageW(
    dwFlags: DWORD, lpSource: LPCVOID, dwMessageId: DWORD, dwLanguageId: DWORD,
    lpBuffer: LPWSTR, nSize: DWORD, Arguments: va_list,
  ) -> DWORD;
}
}

Now let's go through each argument one by one.


#![allow(unused)]
fn main() {
impl core::fmt::Display for Win32Error {
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    let dwFlags = todo!();
    let lpSource = todo!();
    let dwMessageId = todo!();
    let dwLanguageId = todo!();
    let lpBuffer = todo!();
    let nSize = todo!();
    let Arguments = todo!();
    let dword = unsafe {
      FormatMessageW(
        dwFlags,
        lpSource,
        dwMessageId,
        dwLanguageId,
        lpBuffer,
        nSize,
        Arguments,
      )
    };
    todo!("call FormatMessageW")
  }
}
}
  • dwFlags lets us control a lot of options. Looking carefully, it seems like we want FORMAT_MESSAGE_ALLOCATE_BUFFER, which makes FormatMessageW perform the allocation of a large enough buffer. But then... if we use this lpBuffer gets a little odd. More on that in a moment. We also want FORMAT_MESSAGE_FROM_SYSTEM, since these are system errors. All done? Not quite. If we skip ahead down the page to "Security Remarks" then we see that we need FORMAT_MESSAGE_IGNORE_INSERTS too. Now are we done? Hmm, if we set the low-order byte we can fiddle the line length, but we don't need that. We'll leave it as 0.
  • lpSource is the location of the message definition. It's only used if our message is from an hmodule or a string. Since our message is from the system the argument is ignored, so we'll leave this as null.
  • dwMessageId is the identifier of the requested message. That means the error code, so we'll set self.0 here.
  • dwLanguageId is the language identifier of the message. Happily, if we just pass 0, then it'll basically look up the best message it can, and then format that. So we'll just pass 0.
  • lpBuffer is... hey we had to remember something about this! Okay so because we're using FORMAT_MESSAGE_ALLOCATE_BUFFER... well normally this would be interpreted as LPWSTR (pointer to a null-terminated wide string). However, since we're using FORMAT_MESSAGE_ALLOCATE_BUFFER, instead the function will use our pointer as a pointer to the start of a buffer. The lpBuffer is written with the buffer start info, and then we read it back after the function completes, and we get our allocation that way. So, in our use case, the lpBuffer arg is a pointer to a pointer. We have to be careful about this point.
  • nSize is the size of the output buffer, if you're providing the output buffer, or it's the minimum output buffer size you want if you're using FORMAT_MESSAGE_ALLOCATE_BUFFER. We don't have any minimum needs, so we'll give 0.
  • Arguments is the insert arguments for the formatting. However, we're using FORMAT_MESSAGE_IGNORE_INSERTS, so we'll pass null.

Returns: what we get back is the number of TCHAR values stored in the buffer, excluding the final null character. A TCHAR is either an i8 (for A functions) or a u16 (for W functions).

Okay, so, let's review what we've got so far, because this is a lot of little things:


#![allow(unused)]
fn main() {
impl core::fmt::Display for Win32Error {
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    let dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER
      | FORMAT_MESSAGE_FROM_SYSTEM
      | FORMAT_MESSAGE_IGNORE_INSERTS;
    let lpSource = null_mut();
    let dwMessageId = self.0;
    let dwLanguageId = 0;
    let mut buffer: *mut u16 = null_mut();
    let lpBuffer = &mut buffer as *mut *mut u16 as *mut u16;
    let nSize = 0;
    let Arguments = null_mut();
    let tchar_count_excluding_null = unsafe {
      FormatMessageW(
        dwFlags,
        lpSource,
        dwMessageId,
        dwLanguageId,
        lpBuffer,
        nSize,
        Arguments,
      )
    };
    todo!("read the buffer")
  }
}
}

So if we got a count of 0, of if the buffer is still null, then there was some sort of problem.


#![allow(unused)]
fn main() {
if tchar_count_excluding_null == 0 || buffer.is_null() {
  // some sort of problem happened. we can't usefully get_last_error since
  // Display formatting doesn't let you give an error value.
  return Err(core::fmt::Error);
} else {
  todo!()
}
}

If there was no problem then we need to access the buffer. The simplest way is to turn it into a slice:


#![allow(unused)]
fn main() {
    } else {
      let buffer_slice: &[u16] = unsafe {
        core::slice::from_raw_parts(buffer, tchar_count_excluding_null as usize)
      };
      todo!()
    }
}

Now we can decode the data with decode_utf16. This iterates over the u16 values, producing Result<char, DecodeUtf16Error> as it goes. If there was any decoding error, let's just use the standard Unicode Replacement Character instead. Then we put whatever character we've got into the output.


#![allow(unused)]
fn main() {
for decode_result in
  core::char::decode_utf16(buffer_slice.iter().copied())
{
  let ch = decode_result.unwrap_or('�');
  write!(f, "{}", ch)?;
}
}

Cool. All done? Ah, not quite.

Remember how we had FormatMessageW allocate the buffer for us? We need to free that buffer or we'll have a memory leak. A memory leak is safe, but it's still bad.

There's more than one allocation system within Windows. To free this memory, FormatMessageW says that we need to use LocalFree.


#![allow(unused)]
fn main() {
pub type HLOCAL = HANDLE;
#[link(name = "Kernel32")]
extern "system" {
  /// [`LocalFree`](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-localfree)
  pub fn LocalFree(hMem: HLOCAL) -> HLOCAL;
}
}

So where's our call to FreeLocal go? At the end, right? Except we also have all those ? operators on the writing. Any of those can early return from the function.

Let's use the magic of Drop to solve our problem.


#![allow(unused)]
fn main() {
struct OnDropLocalFree(HLOCAL);
impl Drop for OnDropLocalFree {
  fn drop(&mut self) {
    unsafe { LocalFree(self.0) };
  }
}
let _on_drop = OnDropLocalFree(buffer as HLOCAL);
let buffer_slice: &[u16] = unsafe {
  core::slice::from_raw_parts(buffer, tchar_count_excluding_null as usize)
};
for decode_result in
  core::char::decode_utf16(buffer_slice.iter().copied())
{
  let ch = decode_result.unwrap_or('�');
  write!(f, "{}", ch)?;
}
Ok(())
}

Isn't that cool? I think it's pretty cool.

One small note: we have to be sure to bind it to a local variable. If we didn't bind it to a local variable, or if we bound it to the special _ variable, then the struct would drop immediately (before we read the buffer), and then things would go very wrong.

If we test it out with error code 0, we can see "The operation completed successfully.\r\n". Hmm, let's eat up those newline characters though. We didn't want those.


#![allow(unused)]
fn main() {
match decode_result {
  Ok('\r') | Ok('\n') => write!(f, " ")?,
  Ok(ch) => write!(f, "{}", ch)?,
  Err(_) => write!(f, "�")?,
}
}

that's better.

One other note: if the 29th bit is set, then it's an application error. The system doesn't know how to format those, so we won't even ask it. Instead, we'll just show display that and return early.


#![allow(unused)]
fn main() {
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    if self.0 & (1 << 29) > 0 {
      return write!(f, "Win32ApplicationError({})", self.0);
    }
}

We want our error getting function to use this great new type we worked on:


#![allow(unused)]
fn main() {
/// Gets the thread-local last-error code value.
///
/// See [`GetLastError`](https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror)
pub fn get_last_error() -> Win32Error {
  Win32Error(unsafe { GetLastError() })
}
}

And then we can update our stuff that returns Result types to use this error type.


#![allow(unused)]
fn main() {
pub fn load_predefined_cursor(cursor: IDCursor) -> Result<HCURSOR, Win32Error> {
  // ...
}

pub unsafe fn register_class(
  window_class: &WNDCLASSW,
) -> Result<ATOM, Win32Error> {
  // ...
}
}

There's also the std::error::Error trait. It's a bit of a mess right now, but there's a Working Group trying to develop things to be better in the future. At the moment, we might as well implement std::error::Error for our error type, just to be potentially more compatible with the rest of the Rust ecosystem. It's not like we even have to do anything, it's all default methods:


#![allow(unused)]
fn main() {
impl std::error::Error for Win32Error {}
}

Window Creation

The next unsafe function that our main calls is CreateWindowExW. This one is one heck of a swiss-army-chainsaw of a function. See, it turns out that, in Win32, not only are the things we think of as windows "windows", but tons of the GUI elements are "windows" too. It's all windows, everywhere, all over the place.

So CreateWindowExW has like a million options it can do. It also has a ton of arguments that can't be easily verified. It's just as bad as register_class, the only difference is that the arguments are passed as arguments, instead of being stuffed into a struct and then passed as a single struct.

Like we did with with register_class, we're gonna basically skip on the verification and leave it as unsafe. What we will do is give it a Result for the output, so that we enforce the error handling on ourselves.


#![allow(unused)]
fn main() {
/// Creates a window.
///
/// See [`CreateWindowExW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowexw)
pub unsafe fn create_window_ex_w(
  ex_style: DWORD, class_name: LPCWSTR, window_name: LPCWSTR, style: DWORD,
  x: c_int, y: c_int, width: c_int, height: c_int, parent: HWND, menu: HMENU,
  instance: HINSTANCE, param: LPVOID,
) -> Result<HWND, Win32Error> {
  let hwnd = CreateWindowExW(
    ex_style,
    class_name,
    window_name,
    style,
    x,
    y,
    width,
    height,
    parent,
    menu,
    instance,
    param,
  );
  if hwnd.is_null() {
    Err(get_last_error())
  } else {
    Ok(hwnd)
  }
}
}

Ugh. Are we really adding value here? Isn't the point to like, you know, cut down on accidents? Let's simplify this.

  • First of all, we'll just accept &str and then make wide strings ourselves. This lets us use string literals, and the extra allocation isn't a huge deal. We're already calling the OS to make a window, so this isn't a "hot path" function.
  • Next, we won't accept ex_style or style values. We'll just pick some "good default" values to use. Since a user can always just bypass our decision if they really want to (by calling CreateWindowExW themselves), it's fine.
  • Instead of accepting x and y, we'll just take an Option<[i32;2]> as the position. If you give a Some then it uses the two array values as the x and y. If you give a None then both x and y will be CW_USEDEFAULT, which gives a default position. This is much simpler than the normal rules for how CW_USEDEFAULT works. The normal rules seriously take up about two paragraphs of the CreateWindowExW documentation.
  • Also, the window size can be [i32; 2]. It doesn't seem particularly useful to keep the ability to have a default size. It's not a huge burden to have the caller always pick a size.
  • We don't need to specify a parent window. We'll always pass null, so that's one less thing for the caller to think about.
  • We don't need to specify a custom menu to use. A null argument here means to use the class window, so if we wanna change the menu we'd change it on the window class. Again, one less thing for the caller to think about in the 99% case.
  • The instance isn't useful to pass in, we can just have create_app_window look up the instance itself.
  • We'll rename param to create_param. Normally, the styles used can change the meaning of this pointer. With the styles we're using, this will be the argument to the WM_NCCREATE event.

#![allow(unused)]
fn main() {
pub unsafe fn create_app_window(
  class_name: &str, window_name: &str, position: Option<[i32; 2]>,
  [width, height]: [i32; 2], create_param: LPVOID,
) -> Result<HWND, Win32Error> {
  // ...
}
}

That's a lot less for the caller to think about. We can call it at moderate improvement.

Messages

Next would be ShowWindow, but I'm not sure we can provide much help there. We don't have a general window abstraction where we can be sure that a HWND is real or not. So even if we made an enum for the second arg, it'd be an unsafe function overall. There's also no error value to help fix up into an Option or Result. I suppose we'll skip over it for now.

Instead, let's have a look at GetMessageW. Here's a function where I think we can make some improvements.

The basic output of GetMessageW is 0 for a quit event, or non-zero for anything else, and if the "anything else" was an error, then it's specifically -1. That's because it's kinda intended to be used with C's looping constructs, where test expressions evaluating to 0 will cancel the loop. Except, it doesn't work well with C loops because you end up missing the error when you get -1 (which isn't 0, so you'd continue the loop). In fact MSDN specifically tells you to not write while (GetMessage(lpMsg, hWnd, 0, 0)) {, because it does the wrong thing, and presumably enough people wrote that and asked why it went wrong that they put it on the docs to not do that. So I think we can easily say that they picked the wrong sentinel values for GetMessageW to use. Still, they are what they are, we'll just adapt a bit. Instead, let's focus on if we got a message or not, and then we can worry about if it was a quit event in the calling code. What we want is something like this:


#![allow(unused)]
fn main() {
/// Gets a message from the thread's message queue.
///
/// The message can be for any window from this thread,
/// or it can be a non-window message as well.
///
/// See [`GetMessageW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessagew)
#[inline(always)]
pub fn get_any_message() -> Result<MSG, Win32Error> {
  let mut msg = MSG::default();
  let output = unsafe { GetMessageW(&mut msg, null_mut(), 0, 0) };
  if output == -1 {
    Err(get_last_error())
  } else {
    Ok(msg)
  }
}
}

And then in main adjust how we call it just a bit:


#![allow(unused)]
fn main() {
loop {
  match get_any_message() {
    Ok(msg) => {
      if msg.message == WM_QUIT {
        break;
      }
      unsafe {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
      }
    }
    Err(e) => panic!("Error when getting from the message queue: {}", e),
  }
}
}

Next, we can make TranslateMessage safe too.


#![allow(unused)]
fn main() {
/// Translates virtual-key messages into character messages.
///
/// The character messages go into your thread's message queue,
/// and you'll see them if you continue to consume messages.
///
/// **Returns:**
/// * `true` if the message was `WM_KEYDOWN`, `WM_KEYUP`, `WM_SYSKEYDOWN`, or
///   `WM_SYSKEYUP`.
/// * `true` for any other message type that generated a character message.
/// * otherwise `false`
///
/// See [`TranslateMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-translatemessage)
pub fn translate_message(msg: &MSG) -> bool {
  0 != unsafe { TranslateMessage(msg) }
}
}

Can we make DispatchMessageW safe just as easily? Sadly, no. Using DispatchMessageW causes the window procedure to be called, or it can cause a timer callback to be called. Since a call to DispatchMessageW with a funky MSG value could make arbitrary functions get called, and with arbitrary arguments, then we cannot wrap DispatchMessageW in a safe way. In the case of main, we can see that we're not messing with the fields of the message, everything in the message is what the operating system said, so we know the message content is "real" content. However, if we put a safe version of DispatchMessageW into our library, that library code wouldn't actually be correct for all possible message inputs.

Get/Set Window Long Pointer

When we're using SetWindowLongPtrW, and also the Get version, there's a lot of options going on. Also, we're also not checking the error values properly at the moment.

What's supposed to happen with the setter is that you set a value, and the return value is the previous value. If there's an error, then you get 0 back (and you call GetLastError). Except, if the previous value was 0, then you can't tell if things are wrong or not. So what you do is you first call SetLastError, which we haven't used yet, and you set the error code to 0. Then you do SetWindowLongPtrW and if you do get a 0, then you can check the error code. If the error code is still the 0 that you set it to, then actually you had a "successful" 0. The GetWindowLongPtrW behaves basically the same.

For now, we'll only support getting/setting the userdata pointer. This simplifies the problem immensely.

First we need to declare that we'll be using SetLastError


#![allow(unused)]
fn main() {
#[link(name = "Kernel32")]
extern "system" {
  /// [`SetLastError`](https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror)
  pub fn SetLastError(dwErrCode: DWORD);
}
}

And we'll make this callable as a safe operation,


#![allow(unused)]
fn main() {
/// Sets the thread-local last-error code value.
///
/// See [`SetLastError`](https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror)
pub fn set_last_error(e: Win32Error) {
  unsafe { SetLastError(e.0) }
}
}

Now we can make an unsafe function for setting the userdata pointer. We'll make it generic over whatever pointer type you want:


#![allow(unused)]
fn main() {
/// Sets the "userdata" pointer of the window (`GWLP_USERDATA`).
///
/// **Returns:** The previous userdata pointer.
///
/// [`SetWindowLongPtrW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptrw)
pub unsafe fn set_window_userdata<T>(
  hwnd: HWND, ptr: *mut T,
) -> Result<*mut T, Win32Error> {
  set_last_error(Win32Error(0));
  let out = SetWindowLongPtrW(hwnd, GWLP_USERDATA, ptr as LONG_PTR);
  if out == 0 {
    // if output is 0, it's only a "real" error if the last_error is non-zero
    let last_error = get_last_error();
    if last_error.0 != 0 {
      Err(last_error)
    } else {
      Ok(out as *mut T)
    }
  } else {
    Ok(out as *mut T)
  }
}
}

And this lets us upgrade our window creation process a bit:


#![allow(unused)]
fn main() {
// in `window_procedure`
    WM_NCCREATE => {
      println!("NC Create");
      let createstruct: *mut CREATESTRUCTW = lparam as *mut _;
      if createstruct.is_null() {
        return 0;
      }
      let ptr = (*createstruct).lpCreateParams as *mut i32;
      return set_window_userdata::<i32>(hwnd, ptr).is_ok() as LRESULT;
    }
}

The getter for the userdata pointer is basically the same deal. Again, we're making it generic so you can ask for the pointer as pointing to any type, and then it'll do the cast for you. If you forget to say a type you'll get a type inference error, which is good in this case, because you don't want to forget the type:


#![allow(unused)]
fn main() {
/// Gets the "userdata" pointer of the window (`GWLP_USERDATA`).
///
/// **Returns:** The userdata pointer.
///
/// [`GetWindowLongPtrW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowlongptrw)
pub unsafe fn get_window_userdata<T>(hwnd: HWND) -> Result<*mut T, Win32Error> {
  set_last_error(Win32Error(0));
  let out = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
  if out == 0 {
    // if output is 0, it's only a "real" error if the last_error is non-zero
    let last_error = get_last_error();
    if last_error.0 != 0 {
      Err(last_error)
    } else {
      Ok(out as *mut T)
    }
  } else {
    Ok(out as *mut T)
  }
}
}

Now we can adjust how WM_DESTROY and WM_PAINT are handled.


#![allow(unused)]
fn main() {
// in `window_procedure`
    WM_DESTROY => {
      match get_window_userdata::<i32>(hwnd) {
        Ok(ptr) if !ptr.is_null() => {
          Box::from_raw(ptr);
          println!("Cleaned up the box.");
        }
        Ok(_) => {
          println!("userdata ptr is null, no cleanup")
        }
        Err(e) => {
          println!("Error while getting the userdata ptr to clean it up: {}", e)
        }
      }
      PostQuitMessage(0);
    }
    WM_PAINT => {
      match get_window_userdata::<i32>(hwnd) {
        Ok(ptr) if !ptr.is_null() => {
          println!("Current ptr: {}", *ptr);
          *ptr += 1;
        }
        Ok(_) => {
          println!("userdata ptr is null")
        }
        Err(e) => {
          println!("Error while getting the userdata ptr: {}", e)
        }
      }
      let mut ps = PAINTSTRUCT::default();
      let hdc = BeginPaint(hwnd, &mut ps);
      let _success = FillRect(hdc, &ps.rcPaint, (COLOR_WINDOW + 1) as HBRUSH);
      EndPaint(hwnd, &ps);
    }
}

PostQuitMessage

This one is easy to make safe: you give it an exit code, and that exit code goes with the WM_QUIT message you get back later on.

There's nothing that can go wrong, so we just wrap it.


#![allow(unused)]
fn main() {
/// Indicates to the system that a thread has made a request to terminate
/// (quit).
///
/// The exit code becomes the `wparam` of the [`WM_QUIT`] message your message
/// loop eventually gets.
///
/// [`PostQuitMessage`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage)
pub fn post_quit_message(exit_code: c_int) {
  unsafe { PostQuitMessage(exit_code) }
}
}

And then we just put that as the last line of the WM_DESTROY branch.

BeginPaint

Our next target is BeginPaint, which is another thing that's simple to make easier to use when you've got Rust types available.


#![allow(unused)]
fn main() {
/// Prepares the specified window for painting.
///
/// On success: you get back both the [`HDC`] and [`PAINTSTRUCT`]
/// that you'll need for future painting calls (including [`EndPaint`]).
///
/// [`BeginPaint`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-beginpaint)
pub unsafe fn begin_paint(
  hwnd: HWND,
) -> Result<(HDC, PAINTSTRUCT), Win32Error> {
  let mut ps = PAINTSTRUCT::default();
  let hdc = BeginPaint(hwnd, &mut ps);
  if hdc.is_null() {
    Err(get_last_error())
  } else {
    Ok((hdc, ps))
  }
}
}

FillRect

Using FillRect you can paint using an HBRUSH or a system color.

We only want to support the system color path. First we make an enum for all the system colors. This is a little fiddly because some values are named more than once, and so we have to pick just a single canonical name for each value, but it's not too bad:


#![allow(unused)]
fn main() {
/// See [`GetSysColor`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor)
pub enum SysColor {
  _3dDarkShadow = 21,
  _3dLight = 22,
  ActiveBorder = 10,
  ActiveCaption = 2,
  AppWorkspace = 12,
  /// Button face, also "3D face" color.
  ButtonFace = 15,
  /// Button highlight, also "3D highlight" color.
  ButtonHighlight = 20,
  /// Button shadow, also "3D shadow" color.
  ButtonShadow = 16,
  ButtonText = 18,
  CaptionText = 9,
  /// Desktop background color
  Desktop = 1,
  GradientActiveCaption = 27,
  GradientInactiveCaption = 28,
  GrayText = 17,
  Highlight = 13,
  HighlightText = 14,
  HotLight = 26,
  InactiveBorder = 11,
  InactiveCaption = 3,
  InactiveCaptionText = 19,
  InfoBackground = 24,
  InfoText = 23,
  Menu = 4,
  MenuHighlight = 29,
  MenuBar = 30,
  MenuText = 7,
  ScrollBar = 0,
  Window = 5,
  WindowFrame = 6,
  WindowText = 8,
}
}

and then we make a function to fill in with a system color:


#![allow(unused)]
fn main() {
/// Fills a rectangle with the given system color.
///
/// When filling the specified rectangle, this does **not** include the
/// rectangle's right and bottom sides. GDI fills a rectangle up to, but not
/// including, the right column and bottom row, regardless of the current
/// mapping mode.
///
/// [`FillRect`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-fillrect)
pub unsafe fn fill_rect_with_sys_color(
  hdc: HDC, rect: &RECT, color: SysColor,
) -> Result<(), ()> {
  if FillRect(hdc, rect, (color as u32 + 1) as HBRUSH) != 0 {
    Ok(())
  } else {
    Err(())
  }
}
}

EndPaint

You might think that EndPaint has some sort of error code we're ignoring. It returns a BOOL right? Actually when you check the docs, "The return value is always nonzero". In other words, the function might as well return nothing.


#![allow(unused)]
fn main() {
/// See [`EndPaint`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-endpaint)
pub unsafe fn end_paint(hwnd: HWND, ps: &PAINTSTRUCT) {
  EndPaint(hwnd, ps);
}
}

Not a big gain in terms of API quality. However, this way the caller can at least see they're supposed to pass a real paint struct, and not possibly a null pointer. Also, now it's clear that there's no output value. We'll call it a small win.


#![allow(unused)]
fn main() {
// in `window_procedure`
      match begin_paint(hwnd) {
        Ok((hdc, ps)) => {
          let _ = fill_rect_with_sys_color(hdc, &ps.rcPaint, SysColor::Window);
          end_paint(hwnd, &ps);
        }
        Err(e) => {
          println!("Couldn't begin painting: {}", e)
        }
      }
}

A Painting Closure

Is it easy to mess up the whole begin/end painting thing? Yeah, I could see that going wrong. One thing we might want to try is having a function that takes closure to do painting.

The function signature for this is pretty gnarly, because anything with a closure is gnarly.

The closure is going to get three details it needs to know from the PAINTSTRUCT:

  • The HDC.
  • If the background needs to be erased or not.
  • The target rectangle for painting. Everything else in the PAINTSTRUCT is just system reserved info that we don't even care about.

Our library function will get the HWND and the closure. It starts the painting, runs the closure, and then ends the painting. Remember that we want the painting to be ended regardless of success/failure of the closure.


#![allow(unused)]
fn main() {
/// Performs [`begin_paint`] / [`end_paint`] around your closure.
pub unsafe fn do_some_painting<F, T>(hwnd: HWND, f: F) -> Result<T, Win32Error>
where
  F: FnOnce(HDC, bool, RECT) -> Result<T, Win32Error>,
{
  let (hdc, ps) = begin_paint(hwnd)?;
  let output = f(hdc, ps.fErase != 0, ps.rcPaint);
  end_paint(hwnd, &ps);
  output
}
}

Neat!

Note that, to write this, we needed to make RECT a Copy type. Most all the C structs we're declaring should be Debug, Clone, Copy, etc. We just didn't add all the impls at the time.

What's it look like in practice? Not too bad at all:


#![allow(unused)]
fn main() {
// in `window_procedure`
    WM_PAINT => {
      match get_window_userdata(hwnd) {
        Ok(ptr) if !ptr.is_null() => {
          let ptr = ptr as *mut i32;
          println!("Current ptr: {}", *ptr);
          *ptr += 1;
        }
        Ok(_) => {
          println!("userdata ptr is null")
        }
        Err(e) => {
          println!("Error while getting the userdata ptr: {}", e)
        }
      }
      do_some_painting(hwnd, |hdc, _erase_bg, target_rect| {
        let _ = fill_rect_with_sys_color(hdc, &target_rect, SysColor::Window);
        Ok(())
      })
      .unwrap_or_else(|e| println!("error during painting: {}", e));
    }
}

What I like the most about this is that the user can still call begin_paint and end_paint on their own if they want. Because maybe we make some abstraction workflow thing that doesn't work for them, and they can just skip around our thing if that's the case.

Using The Exit Code

One thing we don't do is pass along the wParam from the MSG struct when we see WM_QUIT. We're supposed to pass that as the exit code of our process. For this, we can use std::process:exit, and then pass the value, instead of just breaking the loop.


#![allow(unused)]
fn main() {
// in main
  loop {
    match get_any_message() {
      Ok(msg) => {
        if msg.message == WM_QUIT {
          std::process::exit(msg.wParam as i32);
        }
        translate_message(&msg);
        unsafe {
          DispatchMessageW(&msg);
        }
      }
      Err(e) => panic!("Error when getting from the message queue: {}", e),
    }
  }
}

Done

Is our program perfect? Naw, but I think it's good enough for now.

Loading Open GL

While the usage of Open GL is basically the same across platforms, the precise details of how to first initialize Open GL varies.

The general idea is that we need to do two things:

  1. Create an Open GL context and make it "current". A GL context is what holds all of the drawing state for GL. Each GL Context must only be current in a single thread at a time, otherwise you'll get undefined behavior. Also, a thread's current context is a thread-local variable, so you can't have more than one context current in a thread at a time. Also, a context is associated with a particular window's drawing area. Usually you have just one window, and so you have only one GL context, meaning that don't need to worry about any of that. If you're trying to use GL with more than one window at a time, things can get tricky.
  2. Load the GL function pointers. Unfortunately, you can't access GL like it was a normal dynamic library. At least, you can't on Windows and Linux. We'll have a whole fun time loading function pointers manually.

Acknowledgements

For this portion of the guide I'd like to give an incredible acknowledgement to glowcoil, who wrote the raw-gl-context crate, which gives come very clean and clear examples of how you open a GL context on the major platforms.

Loading OpenGL With Win32

On Windows, turning on OpenGL requires... a few steps.

It's honestly gonna seem borderline silly when I explain it.

Basically, Microsoft would rather that you use DirectX, the Windows-exclusive graphics card API, and so they don't really bother to make it easy to turn on OpenGL.

Documentation

In terms of documentation for opening a GL Context, you can check out MSDN like we've done before, but you'll probably get a little more help from The OpenGL Wiki. Technically, the context creation process is fully outside the OpenGL specification. However, they still explain how to create a context on the wiki because it's obviously a necessary step to use GL.

Expanding the "Cleaned Up" Win32 Example

For this lesson we'll be using the "cleaned up" Win32 example as our starting point.

Device Context

The first thing we need to do is adjust the WNDCLASSW value that we register.

According to the wiki article, we need to set CS_OWNDC in the style field so that each window has its own device context:

// in our fn main()
  // ...
  let mut wc = WNDCLASSW::default();
  wc.style = CS_OWNDC; // NEW
  // ...

If we search for CS_OWNDC in the MSDN pages, it leads us to the Window Class Styles page. Ah, so many things to look at. We know we want CS_OWNDC (Class Style: Own Device Context), but if we glance at the other options there's mostly stuff we don't need. Interestingly, it looks like maybe you need to enable double-click support on your window if you want it? We don't need that now, but just something to remember if you want it later on. There's also CS_HREDRAW and CS_VREDRAW, which cause the full window to be redrawn if the window changes horizontal or vertical size. That seems fairly handy, it means we just always draw everything if the window resizes, and we don't have to think about re-drawing sub-regions. We'll use those too.

All three new declarations can go directly into the win32 module of our library:


#![allow(unused)]
fn main() {
// src/win32.rs

/// Allocates a unique device context for each window in the class.
pub const CS_OWNDC: u32 = 0x0020;

/// Redraws the entire window if a movement or size adjustment changes the width
/// of the client area.
pub const CS_HREDRAW: u32 = 0x0002;

/// Redraws the entire window if a movement or size adjustment changes the
/// height of the client area.
pub const CS_VREDRAW: u32 = 0x0001;
}

Pixel Format Descriptor

Next we need to fill out a PIXELFORMATDESCRIPTOR. This is our request for what we want the eventual pixel format of the window to be. We can't be sure we'll get exactly what we ask for, but if we ask for something fairly common we'll probably get it.

Looking at the MSDN page for PIXELFORMATDESCRIPTOR, there's two very important fields that are different from any struct fields we've seen so far. The nSize and nVersion fields specify the struct's size (in bytes) along with a version number. That's a little... odd.

Versioned Structs

Because of course you know what the size of the struct is, right? You're using struct S from crate foo-x.y.z, and in version x.y.z of crate foo the struct S had... whatever the fields are. If the fields ever were changed, that would have to be published as a new version of the crate, then the compiler would know about the change when you updated the dependency. So there's totally no reason to ever write down the size of the struct within the struct itself, right?

Nope.

This is one area where C still has Rust completely beat: binary stability.

See the folks at Microsoft want programs to last. Like, they want programs to keep working forever if possible. Users have their old video games, or their old art software, or their old whatever software, and those users want their stuff to keep working when there's a Windows Update. And they often won't update to a newer version of Windows if it breaks their stuff. So Microsoft would really rather that all old programs keep working for as long as possible, because they want to keep selling you new versions of Windows. They're usually pretty good at this.

A few months ago I downloaded an old tile editor from an ancient website, and the site said "this was tested on windows 95, it might work on windows 98". I opened it up on my modern Windows 10 machine, and it worked. A few of the GUI buttons looked kinda "off", visually, but it worked. I was able to edit some tiles and export them, despite the program being from so long ago that the author didn't know if it would work on Windows 98. That's a stability success story right there.

On the other hand, Diablo 2 (circa 2000) doesn't run on Windows 10 without the 1.14 patch, because the 1.0 patch level of the game can't launch the program for whatever reason. So the world isn't always as stable as we'd like.

"Lokathor what are you rambling on about? cargo uses SemVer, it's all plenty stable!"

No friends, no, hold on, not quite.

You see cargo builds the whole universe from source every time. When you use foo-x.y.z in your project, cargo downloads the source for that crate with that version, then it builds that crate version right on your own machine, and then it builds your crate and links it all together. This all works out when you're making just one program. I mean, clean build times are huge, but whatever, right?

However, when you're asking two programs to communicate with each other, obviously you need to be able to update one of the programs without updating the other program right away (or ever). Which means that if you ever want to be able to change your message format in the future, you need to put a version on that kind of message now, so that the possibility to change the version number later is left open to you.

"Lokathor! We're only building one program! What are you even talking about!?"

No, friends, the operating system is the other program.

"...what?"

Yeah, the operating system isn't magic. It's just a program. It's a program that runs programs for you. And manages when they get to use the CPU and RAM and stuff. So the word "just" is doing a lot of heavy lifting when I say "it's just a program", but ultimately that's true.

Which means that you'll often see things like the PIXELFORMATDESCRIPTOR struct, where there's something that lets you say what version of the API's protocol you're intending to use when you make the API call. The operating system looks at those signs, and does the right thing for the version you expect.

So you end up seeing this pattern a lot any time there's an interface that's intended to be stable over a long period of time. It's used quite a bit within Win32, and it's all over in Vulkan, for example.

Okay, fun digression, let's get back to the task at hand.

Defining PIXELFORMATDESCRIPTOR

In terms of the fields, the PIXELFORMATDESCRIPTOR type is fairly obvious:


#![allow(unused)]
fn main() {
#[repr(C)]
pub struct PIXELFORMATDESCRIPTOR {
  pub nSize: WORD,
  pub nVersion: WORD,
  pub dwFlags: DWORD,
  pub iPixelType: BYTE,
  pub cColorBits: BYTE,
  pub cRedBits: BYTE,
  pub cRedShift: BYTE,
  pub cGreenBits: BYTE,
  pub cGreenShift: BYTE,
  pub cBlueBits: BYTE,
  pub cBlueShift: BYTE,
  pub cAlphaBits: BYTE,
  pub cAlphaShift: BYTE,
  pub cAccumBits: BYTE,
  pub cAccumRedBits: BYTE,
  pub cAccumGreenBits: BYTE,
  pub cAccumBlueBits: BYTE,
  pub cAccumAlphaBits: BYTE,
  pub cDepthBits: BYTE,
  pub cStencilBits: BYTE,
  pub cAuxBuffers: BYTE,
  pub iLayerType: BYTE,
  pub bReserved: BYTE,
  pub dwLayerMask: DWORD,
  pub dwVisibleMask: DWORD,
  pub dwDamageMask: DWORD,
}
}

However, since we want the type to always say it's size and version, we won't use our unsafe_impl_default_zeroed! that we had before. Instead, we'll have a Default impl that is mostly zeroed memory, and then also sets the size and version for us.


#![allow(unused)]
fn main() {
impl Default for PIXELFORMATDESCRIPTOR {
  #[inline]
  #[must_use]
  fn default() -> Self {
    let mut out: Self = unsafe { core::mem::zeroed() };
    out.nSize = core::mem::size_of::<Self>() as WORD;
    out.nVersion = 1;
    out
  }
}
}

Filling Our Our PIXELFORMATDESCRIPTOR

The wiki page suggests that we fill out our pixel format like this:

PIXELFORMATDESCRIPTOR pfd =
{
	sizeof(PIXELFORMATDESCRIPTOR),
	1,
	PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,    // Flags
	PFD_TYPE_RGBA,        // The kind of framebuffer. RGBA or palette.
	32,                   // Colordepth of the framebuffer.
	0, 0, 0, 0, 0, 0,
	0,
	0,
	0,
	0, 0, 0, 0,
	24,                   // Number of bits for the depthbuffer
	8,                    // Number of bits for the stencilbuffer
	0,                    // Number of Aux buffers in the framebuffer.
	PFD_MAIN_PLANE,
	0,
	0, 0, 0
};

Well, okay, sure, let's just kinda convert that into some Rust:

// after we register the class in fn main
  let pfd = PIXELFORMATDESCRIPTOR {
    dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
    iPixelType: PFD_TYPE_RGBA,
    cColorBits: 32,
    cDepthBits: 24,
    cStencilBits: 8,
    iLayerType: PFD_MAIN_PLANE,
    ..Default::default()
  };

Oh that's actually pretty clear. This time we're using the functional update syntax for struct creation. We haven't used that before, so I'll link the reference there for you. We could also use it in other places, such as filling out our WNDCLASSW value. Honestly, I don't even use it myself that much, but I thought we'd just give it a try, and see how it feels.

Anyway, now we need to declare a bunch of consts.

Except, the PIXELFORMATDESCRIPTOR page on MSDN doesn't list the values.

Guess it's back to grepping through the windows header files. By the way, if you're not aware, there's a very fast grep tool written in rust called ripgrep you might want to try.

C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0>rg "PFD_DRAW_TO_WINDOW"
um\wingdi.h
3640:#define PFD_DRAW_TO_WINDOW          0x00000004

C:\Program Files (x86)\Windows Kits\10\Include\10.0.16299.0>rg "PFD_SUPPORT_OPENGL"
um\wingdi.h
3643:#define PFD_SUPPORT_OPENGL          0x00000020

Looks like it's generally around line 3640 of wingdi.h, so we just have a look at those defines and then do a little Rust.


#![allow(unused)]
fn main() {
/// [`PIXELFORMATDESCRIPTOR`] pixel type
pub const PFD_TYPE_RGBA: u8 = 0;
/// [`PIXELFORMATDESCRIPTOR`] pixel type
pub const PFD_TYPE_COLORINDEX: u8 = 1;

/// [`PIXELFORMATDESCRIPTOR`] layer type
pub const PFD_MAIN_PLANE: u8 = 0;
/// [`PIXELFORMATDESCRIPTOR`] layer type
pub const PFD_OVERLAY_PLANE: u8 = 1;
/// [`PIXELFORMATDESCRIPTOR`] layer type
pub const PFD_UNDERLAY_PLANE: u8 = u8::MAX /* was (-1) */;

pub const PFD_DOUBLEBUFFER: u32 = 0x00000001;
pub const PFD_STEREO: u32 = 0x00000002;
pub const PFD_DRAW_TO_WINDOW: u32 = 0x00000004;
pub const PFD_DRAW_TO_BITMAP: u32 = 0x00000008;
pub const PFD_SUPPORT_GDI: u32 = 0x00000010;
pub const PFD_SUPPORT_OPENGL: u32 = 0x00000020;
pub const PFD_GENERIC_FORMAT: u32 = 0x00000040;
pub const PFD_NEED_PALETTE: u32 = 0x00000080;
pub const PFD_NEED_SYSTEM_PALETTE: u32 = 0x00000100;
pub const PFD_SWAP_EXCHANGE: u32 = 0x00000200;
pub const PFD_SWAP_COPY: u32 = 0x00000400;
pub const PFD_SWAP_LAYER_BUFFERS: u32 = 0x00000800;
pub const PFD_GENERIC_ACCELERATED: u32 = 0x00001000;
pub const PFD_SUPPORT_DIRECTDRAW: u32 = 0x00002000;
pub const PFD_DIRECT3D_ACCELERATED: u32 = 0x00004000;
pub const PFD_SUPPORT_COMPOSITION: u32 = 0x00008000;

/// use with [`ChoosePixelFormat`] only
pub const PFD_DEPTH_DONTCARE: u32 = 0x20000000;
/// use with [`ChoosePixelFormat`] only
pub const PFD_DOUBLEBUFFER_DONTCARE: u32 = 0x40000000;
/// use with [`ChoosePixelFormat`] only
pub const PFD_STEREO_DONTCARE: u32 = 0x80000000;
}

When translating a C #define into a Rust declaration you've gotta use a little judgement. In addition to looking at the raw numeric value, you've gotta try and match the type of the const to the type of the place it gets used the most. The pixel types and layer types get assigned to a field that's a u8, so we declare them as u8. The flags all get combined and assigned to a field that's a u32, so we declare them as a u32. In C it makes little difference (because C numbers will generally coerce automatically), but in Rust we would have to write some casts somewhere if the types don't line up, so we try to make our lives easy later by getting the declaration itself to have the most used type.

ChoosePixelFormat

Okay once we have a PIXELFORMATDESCRIPTOR value we call ChoosePixelFormat to get a "pixel format index" that's the closest available pixel format to our request.

First we declare the external call of course:


#![allow(unused)]
fn main() {
// note: our first use of Gdi32!
#[link(name = "Gdi32")]
extern "system" {
  /// [`ChoosePixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-choosepixelformat)
  pub fn ChoosePixelFormat(
    hdc: HDC, ppfd: *const PIXELFORMATDESCRIPTOR,
  ) -> c_int;
}
}

Of course, this function can fail, so we want to have a Result as the output type in the final version we'll use. Instead of doing a whole thing with the raw calls, and then doing an entire revision phase to make all the "nicer" versions after that, we'll just jump right to the part where we make the nice version as we cover each new function. Personally, I think that seeing the "all raw calls" version of something is a little fun once, but once you start making things Rusty you might as well keep doing it as you go. We won't always know exactly how we want to use each extern function the moment we first see it, but each wrapper function is generally quite small, so we can usually make a change if we realize something new later on.

Let's check the docs:

If the function succeeds, the return value is a pixel format index (one-based) that is the closest match to the given pixel format descriptor. If the function fails, the return value is zero. To get extended error information, call GetLastError.

The docs do not seem to indicate that you're allowed to pass a null pointer for the pixel format descriptor, so we can just require a reference instead of requiring a const pointer.

Okay, easy enough:


#![allow(unused)]
fn main() {
/// See [`ChoosePixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-choosepixelformat)
pub unsafe fn choose_pixel_format(
  hdc: HDC, ppfd: &PIXELFORMATDESCRIPTOR,
) -> Result<c_int, Win32Error> {
  let index = ChoosePixelFormat(hdc, ppfd);
  if index != 0 {
    Ok(index)
  } else {
    Err(get_last_error())
  }
}
}

Can we improve it any more than this? The only thing I can think of would be maybe to newtype that c_int value. We could make a PixelFormatIndex or something if we wanted to. I sure thought about it for a while, sitting on the bus. But now that I'm at the keyboard, it doesn't seem very error prone even as just a c_int. I think we're fine without doing that extra work.

Code you don't write at all is more valuable than code you do write. Or, something wise sounding like that.

Alright so we're gonna set up a PIXELFORMATDESCRIPTOR and choose a pixel format:


#![allow(unused)]
fn main() {
  let pfd = PIXELFORMATDESCRIPTOR {
    dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
    iPixelType: PFD_TYPE_RGBA,
    cColorBits: 32,
    cDepthBits: 24,
    cStencilBits: 8,
    iLayerType: PFD_MAIN_PLANE,
    ..Default::default()
  };
  // Oops, we don't have an HDC value yet!
  let pixel_format_index = unsafe { choose_pixel_format(hdc, &pfd) }.unwrap();
}

AGH! With no HDC from anywhere we can't choose a pixel format! Blast, and such.

We Need An HDC

Alright since we need an HDC to choose a pixel format, we'll move the choosing after we create our window, before we show it, and then we can get the DC for our window, and it'll all be fine.

We'll need to use GetDC to get the HDC value for our window, and then eventually ReleaseDC at some point.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`GetDC`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc)
  pub fn GetDC(hWnd: HWND) -> HDC;

  /// [`ReleaseDC`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc)
  pub fn ReleaseDC(hWnd: HWND, hDC: HDC) -> c_int;
}
}

Okay, now we're ready. So we're going to get the DC for our window, choose a pixel format, we'll set that pixel format (see next section), and then we're all good, right?

No.

Again, of course, this has to be more complicated than that.

If we glance back at the wiki we'll see a section called "Proper Context Creation" with the following warning:

Warning: Unfortunately, Windows does not allow the user to change the pixel format of a window. You get to set it exactly once. Therefore, if you want to use a different pixel format from the one your fake context used (for sRGB or multisample framebuffers, or just different bit-depths of buffers), you must destroy the window entirely and recreate it after we are finished with the dummy context.

...oookay

Sure, we'll just do that thing.

Making A Fake Window

So in between registering the class and making the i32 box, we've got quite a bit of new middle work

// in fn main
  let _atom = unsafe { register_class(&wc) }.unwrap();

  // fake window stuff
  let pfd = PIXELFORMATDESCRIPTOR {
    dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
    iPixelType: PFD_TYPE_RGBA,
    cColorBits: 32,
    cDepthBits: 24,
    cStencilBits: 8,
    iLayerType: PFD_MAIN_PLANE,
    ..Default::default()
  };
  let fake_hwnd = unsafe {
    create_app_window(
      sample_window_class,
      "Fake Window",
      None,
      [1, 1],
      null_mut(),
    )
  }
  .unwrap();
  let fake_hdc = unsafe { GetDC(fake_hwnd) };
  let pf_index = unsafe { choose_pixel_format(fake_hdc, &pfd) }.unwrap();
  // TODO: SetPixelFormat
  assert!(unsafe { ReleaseDC(fake_hwnd, fake_hdc) } != 0);
  assert!(unsafe { DestroyWindow(fake_hwnd) } != 0);

  // real window stuff
  let lparam: *mut i32 = Box::leak(Box::new(5_i32));

Okay, I guess that makes sense.

Hmm, I don't like those mysterious != 0 parts. Let's wrap those into our lib. And since we're wrapping ReleaseDC we might as well do GetDC too.


#![allow(unused)]
fn main() {
/// See [`GetDC`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc)
pub unsafe fn get_dc(hwnd: HWND) -> Option<HDC> {
  let hdc = GetDC(hwnd);
  if hdc.is_null() {
    None
  } else {
    Some(hdc)
  }
}

/// See [`ReleaseDC`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-releasedc)
#[must_use]
pub unsafe fn release_dc(hwnd: HWND, hdc: HDC) -> bool {
  let was_released = ReleaseDC(hwnd, hdc);
  was_released != 0
}

/// See [`DestroyWindow`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow)
pub unsafe fn destroy_window(hwnd: HWND) -> Result<(), Win32Error> {
  let destroyed = DestroyWindow(hwnd);
  if destroyed != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}
}

Here the output isn't always a result. Since GetDC doesn't have any error code to report (according to its docs), we just use Option. Similarly, ReleaseDC doesn't seem to report an error code, so we just return a bool. However, in both cases we want to encourage the caller to actually use the output, because not checking these could lead to nasty memory leaks. So we use the #[must_use] attribute to ensure that they get a warning if they don't use the output value.

Which means our program looks more like this now:


#![allow(unused)]
fn main() {
  let fake_hdc = unsafe { get_dc(fake_hwnd) }.unwrap();
  let pf_index = unsafe { choose_pixel_format(fake_hdc, &pfd) }.unwrap();
  // TODO: SetPixelFormat
  assert!(unsafe { release_dc(fake_hwnd, fake_hdc) });
  unsafe { destroy_window(fake_hwnd) }.unwrap();
}

SetPixelFormat

Once we've chosen a pixel format, we set it to the HDC with SetPixelFormat.


#![allow(unused)]
fn main() {
#[link(name = "Gdi32")]
extern "system" {
  /// [`SetPixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setpixelformat)
  pub fn SetPixelFormat(
    hdc: HDC, format: c_int, ppfd: *const PIXELFORMATDESCRIPTOR,
  ) -> BOOL;
}
}

Which we wrap up a little nicer like this:


#![allow(unused)]
fn main() {
/// Sets the pixel format of an HDC.
///
/// * If it's a window's HDC then it sets the pixel format of the window.
/// * You can't set a window's pixel format more than once.
/// * Call this *before* creating an OpenGL context.
/// * OpenGL windows should use [`WS_CLIPCHILDREN`] and [`WS_CLIPSIBLINGS`]
/// * OpenGL windows should *not* use `CS_PARENTDC`
///
/// See [`SetPixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-setpixelformat)
pub unsafe fn set_pixel_format(
  hdc: HDC, format: c_int, ppfd: &PIXELFORMATDESCRIPTOR,
) -> Result<(), Win32Error> {
  let success = SetPixelFormat(hdc, format, ppfd);
  if success != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}
}

Oh, what's that? Yeah we need some extra window styles.


#![allow(unused)]
fn main() {
/// Excludes the area occupied by child windows when drawing occurs within the
/// parent window.
///
/// This style is used when creating the parent window.
pub const WS_CLIPCHILDREN: u32 = 0x02000000;

/// Clips child windows relative to each other.
///
/// That is, when a particular child window receives a WM_PAINT message,
/// the WS_CLIPSIBLINGS style clips all other overlapping child windows out of
/// the region of the child window to be updated. If WS_CLIPSIBLINGS is not
/// specified and child windows overlap, it is possible, when drawing within the
/// client area of a child window, to draw within the client area of a
/// neighboring child window.
pub const WS_CLIPSIBLINGS: u32 = 0x04000000;

pub unsafe fn create_app_window(/* ... */) {
  // ...
  let hwnd = CreateWindowExW(
    WS_EX_APPWINDOW | WS_EX_OVERLAPPEDWINDOW,
    class_name_null.as_ptr(),
    window_name_null.as_ptr(),
    // New Style!
    WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
    x,
    y,
    width,
    height,
    null_mut(),
    null_mut(),
    get_process_handle(),
    create_param,
  );
  // ...
}
}

Okay, so now we can set the pixel format on our fake HDC:


#![allow(unused)]
fn main() {
  let fake_hdc = unsafe { get_dc(fake_hwnd) }.unwrap();
  let pf_index = unsafe { choose_pixel_format(fake_hdc, &pfd) }.unwrap();
  unsafe { set_pixel_format(fake_hdc, pf_index, &pfd) }.unwrap();
  assert!(unsafe { release_dc(fake_hwnd, fake_hdc) });
  unsafe { destroy_window(fake_hwnd) }.unwrap();
}

Hmm, wait, hold on. So we choose a pixel format, and get an index. How do we check if that index is close to what we wanted? Ah, the docs say we need DescribePixelFormat. This is one of those "does two things at once" functions. When it succeeds, not only is the return code a non-zero value, it's the maximum index of pixel formats. So you can call the function with a null pointer to just get the maximum index. We'll split this up into two different functions.


#![allow(unused)]
fn main() {
/// Gets the maximum pixel format index for the HDC.
///
/// Pixel format indexes are 1-based.
///
/// To print out info on all the pixel formats you'd do something like this:
/// ```no_run
/// # use triangle_from_scratch::win32::*;
/// let hdc = todo!("create a window to get an HDC");
/// let max = unsafe { get_max_pixel_format_index(hdc).unwrap() };
/// for index in 1..=max {
///   let pfd = unsafe { describe_pixel_format(hdc, index).unwrap() };
///   todo!("print the pfd info you want to know");
/// }
/// ```
///
/// See [`DescribePixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-describepixelformat)
pub unsafe fn get_max_pixel_format_index(
  hdc: HDC,
) -> Result<c_int, Win32Error> {
  let max_index = DescribePixelFormat(
    hdc,
    1,
    size_of::<PIXELFORMATDESCRIPTOR>() as _,
    null_mut(),
  );
  if max_index == 0 {
    Err(get_last_error())
  } else {
    Ok(max_index)
  }
}

/// Gets the pixel format info for a given pixel format index.
///
/// See [`DescribePixelFormat`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-describepixelformat)
pub unsafe fn describe_pixel_format(
  hdc: HDC, format: c_int,
) -> Result<PIXELFORMATDESCRIPTOR, Win32Error> {
  let mut pfd = PIXELFORMATDESCRIPTOR::default();
  let max_index = DescribePixelFormat(
    hdc,
    format,
    size_of::<PIXELFORMATDESCRIPTOR>() as _,
    &mut pfd,
  );
  if max_index == 0 {
    Err(get_last_error())
  } else {
    Ok(pfd)
  }
}
}

So we'll print out our pixel format info when we boot the program, seems neat to know. We just throw a #[derive(Debug)] on the PIXELFORMATDESCRIPTOR struct and add a little bit to our main.


#![allow(unused)]
fn main() {
  if let Ok(pfd) = unsafe { describe_pixel_format(fake_hdc, pf_index) } {
    println!("{:?}", pfd);
  } else {
    println!("Error: Couldn't get pixel format description.");
  }
}

Let's give this a try and see what pixel format info prints out:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 1.34s
     Running `target\debug\triangle-from-scratch.exe`
NC Create
Create
PIXELFORMATDESCRIPTOR { nSize: 40, nVersion: 1, dwFlags: 33317, iPixelType: 0, cColorBits: 32, cRedBits: 8, cRedShift: 16, cGreenBits: 8, cGreenShift: 8, cBlueBits: 8, cBlueShift: 0, cAlphaBits: 0, cAlphaShift: 0, cAccumBits: 64, cAccumRedBits: 16, cAccumGreenBits: 16, cAccumBlueBits: 16, cAccumAlphaBits: 16, cDepthBits: 24, cStencilBits: 8, cAuxBuffers: 4, iLayerType: 0, bReserved: 0, dwLayerMask: 0, dwVisibleMask: 0, dwDamageMask: 0 }
userdata ptr is null, no cleanup
NC Create
Create

Uh... huh? So we're seeing the info, but there's no window! Some sort of problem has prevented the real window from showing up. If we comment out all the "fake window" stuff the real window comes back, so some part of that code is at fault here.

Hmm.

What if we make a fake window class to go with our fake window?

fn main() {
  // fake window stuff
  let fake_window_class = "Fake Window Class";
  let fake_window_class_wn = wide_null(fake_window_class);

  let mut fake_wc = WNDCLASSW::default();
  fake_wc.style = CS_OWNDC;
  fake_wc.lpfnWndProc = Some(DefWindowProcW);
  fake_wc.hInstance = get_process_handle();
  fake_wc.lpszClassName = fake_window_class_wn.as_ptr();

  let _atom = unsafe { register_class(&fake_wc) }.unwrap();

  let pfd = // ...
  let fake_hwnd = unsafe {
    create_app_window(
      fake_window_class,
      "Fake Window",
      None,
      [1, 1],
      null_mut(),
    )
  }
  .unwrap();

Okay, now it works. Not sure what the difference is here, but I guess we can investigate that later. Only little problem is that now we have an extra window class floating around. If we use UnregisterClassW we can clean that up.

With UnregisterClassW you can pass in a pointer to a class name, or you can pass in an atom value. We'll make a separate function for each style.


#![allow(unused)]
fn main() {
#[link(name = "User32")]
extern "system" {
  /// [`UnregisterClassW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterclassw)
  pub fn UnregisterClassW(lpClassName: LPCWSTR, hInstance: HINSTANCE) -> BOOL;
}

/// Un-registers the window class from the `HINSTANCE` given.
///
/// * The name must be the name of a registered window class.
/// * This requires re-encoding the name to null-terminated utf-16, which
///   allocates. Using [`unregister_class_by_atom`] instead does not allocate,
///   if you have the atom available.
/// * Before calling this function, an application must destroy all windows
///   created with the specified class.
///
/// See
/// [`UnregisterClassW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterclassw)
pub unsafe fn unregister_class_by_name(
  name: &str, instance: HINSTANCE,
) -> Result<(), Win32Error> {
  let name_null = wide_null(name);
  let out = UnregisterClassW(name_null.as_ptr(), instance);
  if out != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}

/// Un-registers the window class from the `HINSTANCE` given.
///
/// * The atom must be the atom of a registered window class.
/// * Before calling this function, an application must destroy all windows
///   created with the specified class.
///
/// See [`UnregisterClassW`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterclassw)
pub unsafe fn unregister_class_by_atom(
  a: ATOM, instance: HINSTANCE,
) -> Result<(), Win32Error> {
  let out = UnregisterClassW(a as LPCWSTR, instance);
  if out != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}
}

And here's another thing that went wrong when I tried to figure this "window doesn't show up" problem. At one point I was registering the same class twice, because my fake_wc had the same name string as the "real" wc. Then I got this error:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target\debug\triangle-from-scratch.exe`        
PIXELFORMATDESCRIPTOR { nSize: 40, nVersion: 1, dwFlags: 33317, iPixelType: 0, cColorBits: 32, cRedBits: 8, cRedShift: 16, cGreenBits: 8, cGreenShift: 8, cBlueBits: 8, cBlueShift: 0, cAlphaBits: 0, cAlphaShift: 0, cAccumBits: 64, cAccumRedBits: 16, cAccumGreenBits: 16, cAccumBlueBits: 16, cAccumAlphaBits: 16, cDepthBits: 24, cStencilBits: 8, cAuxBuffers: 4, iLayerType: 0, bReserved: 0, dwLayerMask: 0, dwVisibleMask: 0, dwDamageMask: 0 }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Win32Error(1410)', src\main.rs:63:46
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\triangle-from-scratch.exe` (exit code: 101)

Not helpful! Error 1410, what on Earth does that mean? So we should adjust the error lookups to happen in Debug as well as Display.


#![allow(unused)]
fn main() {
#[repr(transparent)]
pub struct Win32Error(pub DWORD);
impl std::error::Error for Win32Error {}

impl core::fmt::Debug for Win32Error {
  /// Displays the error using `FormatMessageW`
  ///
  /// ```
  /// use triangle_from_scratch::win32::*;
  /// let s = format!("{:?}", Win32Error(0));
  /// assert_eq!("The operation completed successfully.  ", s);
  /// let app_error = format!("{:?}", Win32Error(1 << 29));
  /// assert_eq!("Win32ApplicationError(536870912)", app_error);
  /// ```
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    // everything from before
  }
}
impl core::fmt::Display for Win32Error {
  /// Same as `Debug` impl
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    write!(f, "{:?}", self)
  }
}
}

But what if you really wanted that error number. Well, maybe you do, and for that, we can use the "alternate" flag.


#![allow(unused)]
fn main() {
  fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
    // ...
    if f.alternate() {
      return write!(f, "Win32Error({})", self.0);
    }
}

Now if you format with "{:?}" (which is what unwrap uses) then you get the message form, and if you really want to see the number you can format with "{:#?}".

Now when an unwrap goes bad it looks like this:

D:\dev\triangle-from-scratch>cargo run
   Compiling triangle-from-scratch v0.1.0 (D:\dev\triangle-from-scratch)
    Finished dev [unoptimized + debuginfo] target(s) in 0.87s
     Running `target\debug\triangle-from-scratch.exe`
PIXELFORMATDESCRIPTOR { nSize: 40, nVersion: 1, dwFlags: 33317, iPixelType: 0, cColorBits: 32, cRedBits: 8, cRedShift: 16, cGreenBits: 8, cGreenShift: 8, cBlueBits: 8, cBlueShift: 0, cAlphaBits: 0, cAlphaShift: 0, cAccumBits: 64, cAccumRedBits: 16, cAccumGreenBits: 16, cAccumBlueBits: 16, cAccumAlphaBits: 16, cDepthBits: 24, cStencilBits: 8, cAuxBuffers: 4, iLayerType: 0, bReserved: 0, dwLayerMask: 0, dwVisibleMask: 0, dwDamageMask: 0 }
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Class already exists.  ', src\main.rs:63:46
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\triangle-from-scratch.exe` (exit code: 101)

Ah, look, the class already existed, of course!

So finally we can set a pixel format on a fake window, and then clean it all up, and then make our real window.

wglCreateContext

As fun as it is to make a fake window and do not much with it and then throw it away, it might be even better if we did something with it.

The point of all this is that we want to be able to create an OpenGL context with the latest version, and other advanced features. However, Windows only lets you directly make an OpenGL 1.1 context. To make a context with a newer version than that, you need to use an extension. To use an extension, you need to check the extension string to see what extensions are available. To check the extension string, you need to have a current OpenGL context.

What we do is we use our fake window to make a fake GL context, which will be for the old OpenGL 1.1, then we can get the extension string to check what extensions are available. This lets us use the "advanced" capabilities like "making a context with a modern API version".

I told you at the start that this was gonna seem silly when I explained what was going on. I warned you about stairs bro!

Anyway, once we've gotten the info we need from the fake context then we close it, and we close out all the other "fake" stuff, and then we make the "real" stuff.

That means our next step is wglCreateContext, and also the inverse, wglDeleteContext.


#![allow(unused)]
fn main() {
/// Handle (to a) GL Rendering Context
pub type HGLRC = HANDLE;

#[link(name = "Opengl32")]
extern "system" {
  /// [`wglCreateContext`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglcreatecontext)
  pub fn wglCreateContext(Arg1: HDC) -> HGLRC;

  /// [`wglDeleteContext`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wgldeletecontext)
  pub fn wglDeleteContext(Arg1: HGLRC) -> BOOL;
}

/// See [`wglCreateContext`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglcreatecontext)
pub unsafe fn wgl_create_context(hdc: HDC) -> Result<HGLRC, Win32Error> {
  let hglrc = wglCreateContext(hdc);
  if hglrc.is_null() {
    Err(get_last_error())
  } else {
    Ok(hglrc)
  }
}

/// Deletes a GL Context.
///
/// * You **cannot** use this to delete a context current in another thread.
/// * You **can** use this to delete the current thread's context. The context
///   will be made not-current automatically before it is deleted.
///
/// See
/// [`wglDeleteContext`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wgldeletecontext)
pub unsafe fn wgl_delete_context(hglrc: HGLRC) -> Result<(), Win32Error> {
  let success = wglDeleteContext(hglrc);
  if success != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}
}

And then in main:


#![allow(unused)]
fn main() {
  unsafe { set_pixel_format(fake_hdc, pf_index, &pfd) }.unwrap();
  let fake_hglrc = unsafe { wgl_create_context(fake_hdc) }.unwrap();

  // TODO: work with the fake context.

  // cleanup the fake stuff
  unsafe { wgl_delete_context(fake_hglrc) }.unwrap();
  assert!(unsafe { release_dc(fake_hwnd, fake_hdc) });
  unsafe { destroy_window(fake_hwnd) }.unwrap();
  unsafe { unregister_class_by_atom(fake_atom, instance) }.unwrap();
}

wglMakeCurrent

It's no use making and destroying this fake context if we don't make it current:


#![allow(unused)]
fn main() {
#[link(name = "Opengl32")]
extern "system" {
  /// [`wglMakeCurrent`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglmakecurrent)
  pub fn wglMakeCurrent(hdc: HDC, hglrc: HGLRC) -> BOOL;
}

/// Makes a given HGLRC current in the thread and targets it at the HDC given.
///
/// * You can safely pass `null_mut` for both parameters if you wish to make no
///   context current in the thread.
///
/// See
/// [`wglMakeCurrent`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglmakecurrent)
pub unsafe fn wgl_make_current(
  hdc: HDC, hglrc: HGLRC,
) -> Result<(), Win32Error> {
  let success = wglMakeCurrent(hdc, hglrc);
  if success != 0 {
    Ok(())
  } else {
    Err(get_last_error())
  }
}
}

Before we can use the context we have to make it current, and before we destroy it we have to make it not be current. On Windows we don't have to make it not-current, but it's just good habit because on other systems you must make a context not-current before you destroy it.


#![allow(unused)]
fn main() {
  let fake_hglrc = unsafe { wgl_create_context(fake_hdc) }.unwrap();
  unsafe { wgl_make_current(fake_hdc, fake_hglrc) }.unwrap();

  // TODO: work with the fake context.

  // cleanup the fake stuff
  unsafe { wgl_make_current(null_mut(), null_mut()) }.unwrap();
  unsafe { wgl_delete_context(fake_hglrc) }.unwrap();
}

wglGetProcAddress

This is what we've been after the whole time!


#![allow(unused)]
fn main() {
// macros.rs

/// Turns a rust string literal into a null-terminated `&[u8]`.
#[macro_export]
macro_rules! c_str {
  ($text:expr) => {{
    concat!($text, '\0').as_bytes()
  }};
}

// win32.rs

/// Pointer to an ANSI string.
pub type LPCSTR = *const c_char;
/// Pointer to a procedure of unknown type.
pub type PROC = *mut c_void;

#[link(name = "Opengl32")]
extern "system" {
  /// [`wglGetProcAddress`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-wglgetprocaddress)
  pub fn wglGetProcAddress(Arg1: LPCSTR) -> PROC;
}

/// Gets a GL function address.
///
/// The input should be a null-terminated function name string. Use the
/// [`c_str!`] macro for assistance.
///
/// * You must have an active GL context for this to work. Otherwise you will
///   always get an error.
/// * The function name is case sensitive, and spelling must be exact.
/// * All outputs are context specific. Functions supported in one rendering
///   context are not necessarily supported in another.
/// * The extension function addresses are unique for each pixel format. All
///   rendering contexts of a given pixel format share the same extension
///   function addresses.
///
/// This *will not* return function pointers exported by `OpenGL32.dll`, meaning
/// that it won't return OpenGL 1.1 functions. For those old function, use
/// [`GetProcAddress`].
pub fn wgl_get_proc_address(func_name: &[u8]) -> Result<PROC, Win32Error> {
  // check that we end the slice with a \0 as expected.
  match func_name.last() {
    Some(b'\0') => (),
    _ => return Err(Win32Error(Win32Error::APPLICATION_ERROR_BIT)),
  }
  // Safety: we've checked that the end of the slice is null-terminated.
  let proc = unsafe { wglGetProcAddress(func_name.as_ptr().cast()) };
  match proc as usize {
    // Some non-zero values can also be errors,
    // https://www.khronos.org/opengl/wiki/Load_OpenGL_Functions#Windows
    0 | 1 | 2 | 3 | usize::MAX => return Err(get_last_error()),
    _ => Ok(proc),
  }
}
}

This part is pretty simple. We get a value back, and on success it's a pointer to a function. We'll have to use transmute to change the type into the proper function type, but that's a concern for the caller to deal with.

wglGetExtensionsStringARB

Okay, now we can get function pointers. This lets us check for platform specific extensions using the wglGetExtensionsStringARB function.

For this, we'll want a helper macro to make C-style string literals for us:


#![allow(unused)]
fn main() {
/// Turns a rust string literal into a null-terminated `&[u8]`.
#[macro_export]
macro_rules! c_str {
  ($text:expr) => {{
    concat!($text, '\0').as_bytes()
  }};
}
}

And then we:

  • lookup the function
  • call the function
  • get the info from the string pointer we get back

This part is kinda a lot when you just write it all inline in main:


#![allow(unused)]
fn main() {
  unsafe { wgl_make_current(fake_hdc, fake_hglrc) }.unwrap();

  #[allow(non_camel_case_types)]
  type wglGetExtensionsStringARB_t =
    unsafe extern "system" fn(HDC) -> *const c_char;
  let wgl_get_extension_string_arb: Option<wglGetExtensionsStringARB_t> = unsafe {
    core::mem::transmute(
      wgl_get_proc_address(c_str!("wglGetExtensionsStringARB")).unwrap(),
    )
  };
  let mut extension_string: *const u8 =
    unsafe { (wgl_get_extension_string_arb.unwrap())(fake_hdc) }.cast();
  assert!(!extension_string.is_null());
  let mut s = String::new();
  unsafe {
    while *extension_string != 0 {
      s.push(*extension_string as char);
      extension_string = extension_string.add(1);
    }
  }
  println!("> Extension String: {}", s);

  // cleanup the fake stuff
}

but if we break it down it's not so bad. First let's put a function for gathering up a null-terminated byte string into our library. This isn't Win32 specific, so we'll put it in lib.rs:


#![allow(unused)]
fn main() {
/// Gathers up the bytes from a pointer.
///
/// The byte sequence must be null-terminated.
///
/// The output excludes the null byte.
pub unsafe fn gather_null_terminated_bytes(mut p: *const u8) -> Vec<u8> {
  let mut v = vec![];
  while *p != 0 {
    v.push(*p);
    p = p.add(1);
  }
  v
}
}

Now that we have a Vec<u8> we want a String. We can use String::from_utf8, but that returns a Result (it fails if the bytes aren't valid utf8). There's also String::from_utf8_lossy, but if the bytes are valid utf8 then we get a borrow on our vec and we'd have to clone it to get the String. What we want is to move the Vec if we can, and only allocate a new Vec if we must. You'd think that this is a completely common thing to want, but for whatever reason it's not in the standard library.


#![allow(unused)]
fn main() {
// PS: naming is hard :(

/// Converts a `Vec<u8>` into a `String` using the minimum amount of
/// re-allocation.
pub fn min_alloc_lossy_into_string(bytes: Vec<u8>) -> String {
  match String::from_utf8(bytes) {
    Ok(s) => s,
    Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
  }
}
}

Now in win32.rs we'll just use super::*; and make a function to get the extension string:


#![allow(unused)]
fn main() {
/// Gets the WGL extension string for the HDC passed.
///
/// * This relies on [`wgl_get_proc_address`], and so you must have a context
///   current for it to work.
/// * If `wgl_get_proc_address` fails then an Application Error is generated.
/// * If `wgl_get_proc_address` succeeds but the extension string can't be
///   obtained for some other reason you'll get a System Error.
///
/// The output is a space-separated list of extensions that are supported.
///
/// See
/// [`wglGetExtensionsStringARB`](https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_extensions_string.txt)
pub unsafe fn wgl_get_extension_string_arb(
  hdc: HDC,
) -> Result<String, Win32Error> {
  let f: wglGetExtensionsStringARB_t = core::mem::transmute(
    wgl_get_proc_address(c_str!("wglGetExtensionsStringARB"))?,
  );
  let p: *const u8 =
    (f.ok_or(Win32Error(Win32Error::APPLICATION_ERROR_BIT))?)(hdc).cast();
  if p.is_null() {
    Err(get_last_error())
  } else {
    let bytes = gather_null_terminated_bytes(p);
    Ok(min_alloc_lossy_into_string(bytes))
  }
}
}

And now we can try to get the extension string with a single call to that:

// main.rs: fn main
  unsafe { wgl_make_current(fake_hdc, fake_hglrc) }.unwrap();

  let res = unsafe { wgl_get_extension_string_arb(fake_hdc) };
  println!("> Extension String Result: {:?}", res);

  // cleanup the fake stuff

And with a little iterator magic:


#![allow(unused)]
fn main() {
  let extensions: Vec<String> =
    unsafe { wgl_get_extension_string_arb(fake_hdc) }
      .map(|s| {
        s.split(' ').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect()
      })
      .unwrap_or(Vec::new());
  println!("> Extensions: {:?}", extensions);
}

Which prints out alright:

D:\dev\triangle-from-scratch>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target\debug\triangle-from-scratch.exe`        
> Got Pixel Format: PIXELFORMATDESCRIPTOR { nSize: 40, nVersion: 1, dwFlags: 33317, iPixelType: 0, cColorBits: 32, cRedBits: 8, cRedShift: 16, cGreenBits: 8, cGreenShift: 8, cBlueBits: 8, cBlueShift: 0, cAlphaBits: 0, cAlphaShift: 0, cAccumBits: 64, cAccumRedBits: 16, cAccumGreenBits: 16, cAccumBlueBits: 16, cAccumAlphaBits: 16, cDepthBits: 24, cStencilBits: 8, cAuxBuffers: 4, iLayerType: 0, bReserved: 0, dwLayerMask: 0, dwVisibleMask: 0, dwDamageMask: 0 }
> Extensions: ["WGL_ARB_buffer_region", "WGL_ARB_create_context", "WGL_ARB_create_context_no_error", "WGL_ARB_create_context_profile", "WGL_ARB_create_context_robustness", "WGL_ARB_context_flush_control", "WGL_ARB_extensions_string", "WGL_ARB_make_current_read", "WGL_ARB_multisample", "WGL_ARB_pbuffer", "WGL_ARB_pixel_format", "WGL_ARB_pixel_format_float", "WGL_ARB_render_texture", "WGL_ATI_pixel_format_float", "WGL_EXT_colorspace", "WGL_EXT_create_context_es_profile", "WGL_EXT_create_context_es2_profile", "WGL_EXT_extensions_string", "WGL_EXT_framebuffer_sRGB", "WGL_EXT_pixel_format_packed_float", "WGL_EXT_swap_control", "WGL_EXT_swap_control_tear", "WGL_NVX_DX_interop", "WGL_NV_DX_interop", "WGL_NV_DX_interop2", "WGL_NV_copy_image", "WGL_NV_delay_before_swap", "WGL_NV_float_buffer", "WGL_NV_multisample_coverage", "WGL_NV_render_depth_texture", "WGL_NV_render_texture_rectangle"]

Grab Some Function Pointers

Now that we can see what WGL extensions are available, we can grab out some function pointers.

Here's the key part: We don't call them yet.

This is another silly thing, but it's true. We just get the function pointers for now. Then we destroy the fake stuff, then we use the function pointers that we stored to make our real stuff.

We want to get function pointers for:

  • wglChoosePixelFormatARB (provided by WGL_ARB_pixel_format) is required to choose advanced pixel formats (such as multisampling).
  • wglCreateContextAttribsARB (provided by WGL_ARB_create_context) is required to make GL contexts with API versions above 1.1.
  • wglSwapIntervalEXT (provided by WGL_EXT_swap_control) is not required but is very handy, because it lets you enable vsync

These are our core three extension functions. Many of the extensions listed above don't add new functions, they just extend what values you can send to these three.

First we declare the types we'll be using:


#![allow(unused)]
fn main() {
// lib.rs

/// Type for [wglChoosePixelFormatARB](https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt)
pub type wglChoosePixelFormatARB_t = Option<
  unsafe extern "system" fn(
    hdc: HDC,
    piAttribIList: *const c_int,
    pfAttribFList: *const f32,
    nMaxFormats: UINT,
    piFormats: *mut c_int,
    nNumFormats: *mut UINT,
  ) -> BOOL,
>;
pub type FLOAT = c_float;
pub type c_float = f32;
/// Type for [wglCreateContextAttribsARB](https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt)
pub type wglCreateContextAttribsARB_t = Option<
  unsafe extern "system" fn(
    hDC: HDC,
    hShareContext: HGLRC,
    attribList: *const c_int,
  ) -> HGLRC,
>;
/// Type for [wglSwapIntervalEXT](https://www.khronos.org/registry/OpenGL/extensions/EXT/WGL_EXT_swap_control.txt)
pub type wglSwapIntervalEXT_t =
  Option<unsafe extern "system" fn(interval: c_int) -> BOOL>;
}

And then we store the function pointers:


#![allow(unused)]
fn main() {
  println!("> Extensions: {:?}", extensions);

  let wglChoosePixelFormatARB: wglChoosePixelFormatARB_t = unsafe {
    core::mem::transmute(
      wgl_get_proc_address(c_str!("wglChoosePixelFormatARB")).unwrap(),
    )
  };
  let wglCreateContextAttribsARB: wglCreateContextAttribsARB_t = unsafe {
    core::mem::transmute(
      wgl_get_proc_address(c_str!("wglCreateContextAttribsARB")).unwrap(),
    )
  };
  let wglSwapIntervalEXT: wglSwapIntervalEXT_t = unsafe {
    core::mem::transmute(
      wgl_get_proc_address(c_str!("wglSwapIntervalEXT")).unwrap(),
    )
  };

  // cleanup the fake stuff
}

Alright, I think we're done with all this fake context stuff. We can move on to setting up our real context.

Actually, let's briefly put all that stuff into a single library function.


#![allow(unused)]
fn main() {
/// Grabs out the stuff you'll need to have fun with WGL.
pub fn get_wgl_basics() -> Result<
  (
    Vec<String>,
    wglChoosePixelFormatARB_t,
    wglCreateContextAttribsARB_t,
    wglSwapIntervalEXT_t,
  ),
  Win32Error,
> {
  // ...
}
}

It's just moving all the stuff you've seen over, and then putting in a lot of drop guard types like we saw in format message. There's not much new to talk about, so we'll keep moving.

Our New Window Setup

Alright, so now let's get some useful stuff with our window:


#![allow(unused)]
fn main() {
struct WindowData {
  hdc: HDC,
}
impl Default for WindowData {
  fn default() -> Self {
    unsafe { core::mem::zeroed() }
  }
}
}

Since it's going to be connected to a GL context now we don't want to get and free it with every WM_PAINT. Instead, we'll get it once after the window is created, then stuff it into the WindowData field and leave it there. The WM_DESTROY can clean it up before destroying the window itself.

// fn main
  let lparam: *mut WindowData = Box::leak(Box::new(WindowData::default()));
  let hwnd = unsafe {
    create_app_window(
      sample_window_class,
      "Sample Window Name",
      None,
      [800, 600],
      lparam.cast(),
    )
  }
  .unwrap();
  let hdc = unsafe { get_dc(hwnd) }.unwrap();
  unsafe { (*lparam).hdc = hdc };

And we need to adjust our window procedure:


#![allow(unused)]
fn main() {
    WM_DESTROY => {
      match get_window_userdata::<WindowData>(hwnd) {
        Ok(ptr) if !ptr.is_null() => {
          let window_data = Box::from_raw(ptr);
          let _ = release_dc(hwnd, window_data.hdc);
          println!("Cleaned up the box.");
        }
        Ok(_) => {
          println!("userdata ptr is null, no cleanup")
        }
        Err(e) => {
          println!("Error while getting the userdata ptr for cleanup: {}", e)
        }
      }
      post_quit_message(0);
    }
    WM_PAINT => match get_window_userdata::<WindowData>(hwnd) {
      Ok(ptr) if !ptr.is_null() => {
        // TODO: real painting, eventually
        println!("painting!");
      }
      Ok(_) => {
        println!("userdata ptr is null")
      }
      Err(e) => {
        println!("Error while getting the userdata ptr: {}", e)
      }
    },
}

Finally We Call wglChoosePixelFormatARB

We're finally ready to call our wglChoosePixelFormatARB pointer.

This one is fairly flexible. We can pass a list of integer (key,value) pairs, a list of float (key,value) pairs, and the space to get some outputs.

As far as I can tell, there's no reason in the basic extension for any float attributes to be specified. Other extensions might add options in the future, but there's nothing for us there right now. The int attributes, on the other hand, have many interesting things. For both lists, the function knows the list is over when it sees a 0 in the key position. Also for both lists, we can pass a null instead of a list pointer.

Meanwhile, we can have more than one output if we want. We pass in a count, and a pointer to an array of that length. The function will fill in as many array values as it can. There's also a pointer to an integer that we pass, and it gets written the number of outputs that were found. This could be the full array, but it could also be less than the full array.

Interestingly, using min_const_generics might work here. We could make the array of values to return be a const generic. But we only actually need one pixel format, so we'll just pick the first format they give us.

The wrapper function for this is not complex, but it is tall.


#![allow(unused)]
fn main() {
/// Arranges the data for calling a [`wglChoosePixelFormatARB_t`] procedure.
///
/// * Inputs are slices of [key, value] pairs.
/// * Input slices **can** be empty.
/// * Non-empty slices must have a zero value in the key position of the final
///   pair.
pub unsafe fn do_wglChoosePixelFormatARB(
  f: wglChoosePixelFormatARB_t, hdc: HDC, int_attrs: &[[c_int; 2]],
  float_attrs: &[[FLOAT; 2]],
) -> Result<c_int, Win32Error> {
  let app_err = Win32Error(Win32Error::APPLICATION_ERROR_BIT);
  let i_ptr = match int_attrs.last() {
    Some([k, _v]) => {
      if *k == 0 {
        int_attrs.as_ptr()
      } else {
        return Err(app_err);
      }
    }
    None => null(),
  };
  let f_ptr = match float_attrs.last() {
    Some([k, _v]) => {
      if *k == 0.0 {
        float_attrs.as_ptr()
      } else {
        return Err(app_err);
      }
    }
    None => null(),
  };
  let mut out_format = 0;
  let mut out_format_count = 0;
  let b = (f.ok_or(app_err)?)(
    hdc,
    i_ptr.cast(),
    f_ptr.cast(),
    1,
    &mut out_format,
    &mut out_format_count,
  );
  if b != 0 && out_format_count == 1 {
    Ok(out_format)
  } else {
    Err(get_last_error())
  }
}
}

Now the way we call this thing is that we're gonna have some "base" requirements, then we can look at the extensions and maybe ask for a little more if it's available, then we finalize the list by putting in that zero.

After we get the pixel format index, we can't set it directly, because we need a PIXELFORMATDESCRIPTOR to go with it. First we use describe_pixel_format, then we can set_pixel_format.


#![allow(unused)]
fn main() {
  // base criteria
  let mut int_attribs = vec![
    [WGL_DRAW_TO_WINDOW_ARB, true as _],
    [WGL_SUPPORT_OPENGL_ARB, true as _],
    [WGL_DOUBLE_BUFFER_ARB, true as _],
    [WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB],
    [WGL_COLOR_BITS_ARB, 32],
    [WGL_DEPTH_BITS_ARB, 24],
    [WGL_STENCIL_BITS_ARB, 8],
  ];
  // if sRGB is supported, ask for that
  if wgl_extensions.iter().any(|s| s == "WGL_EXT_framebuffer_sRGB") {
    int_attribs.push([WGL_FRAMEBUFFER_SRGB_CAPABLE_EXT, true as _]);
  };
  // let's have some multisample if we can get it
  if wgl_extensions.iter().any(|s| s == "WGL_ARB_multisample") {
    int_attribs.push([WGL_SAMPLE_BUFFERS_ARB, 1]);
  };
  // finalize our list
  int_attribs.push([0, 0]);
  // choose a format, get the PIXELFORMATDESCRIPTOR, and set it.
  let pix_format = unsafe {
    do_wglChoosePixelFormatARB(wglChoosePixelFormatARB, hdc, &int_attribs, &[])
  }
  .unwrap();
  let pfd = unsafe { describe_pixel_format(hdc, pix_format) }.unwrap();
  unsafe { set_pixel_format(hdc, pix_format, &pfd) }.unwrap();
}

Open That Stupid Context Already (wglCreateContextAttribsARB)

Now that we have a pixel format set, we can create a context.

To create an advanced context, we call wglCreateContextAttribsARB.

It's not too different from the last function we used. We pass a list of (key,value) pairs in, with a 0 key to signal the final entry.

The wrapper for this should look familiar, it's the same basic idea:


#![allow(unused)]
fn main() {
/// Arranges the data for calling a [`wglCreateContextAttribsARB_t`] procedure.
///
/// * The input slice consists of [key, value] pairs.
/// * The input slice **can** be empty.
/// * Any non-empty input must have zero as the key value of the last position.
pub unsafe fn do_wglCreateContextAttribsARB(
  f: wglCreateContextAttribsARB_t, hdc: HDC, hShareContext: HGLRC,
  attribList: &[[i32; 2]],
) -> Result<HGLRC, Win32Error> {
  let app_err = Win32Error(Win32Error::APPLICATION_ERROR_BIT);
  let i_ptr = match attribList.last() {
    Some([k, _v]) => {
      if *k == 0 {
        attribList.as_ptr()
      } else {
        return Err(app_err);
      }
    }
    None => null(),
  };
  let hglrc = (f.ok_or(app_err)?)(hdc, hShareContext, i_ptr.cast());
  if hglrc.is_null() {
    Err(get_last_error())
  } else {
    Ok(hglrc)
  }
}
}

And this time we don't even have to use a vec to store all our settings. We don't have a dynamic number of settings, so a plain array will do fine.


#![allow(unused)]
fn main() {
  // now we create a context.
  const FLAGS: c_int = WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
    | if cfg!(debug_assertions) { WGL_CONTEXT_DEBUG_BIT_ARB } else { 0 };
  let hglrc = unsafe {
    do_wglCreateContextAttribsARB(
      wglCreateContextAttribsARB,
      hdc,
      null_mut(),
      &[
        [WGL_CONTEXT_MAJOR_VERSION_ARB, 3],
        [WGL_CONTEXT_MINOR_VERSION_ARB, 3],
        [WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB],
        [WGL_CONTEXT_FLAGS_ARB, FLAGS],
        [0, 0],
      ],
    )
  }
  .unwrap();
  unsafe { wgl_make_current(hdc, hglrc) }.unwrap();
  unsafe { (*lparam).hglrc = hglrc };
}

I'm here selecting OpenGL 3.3 Core, because some day, when this tutorial is finally over, I'm going to say, "and now you can learn how to do the rest of OpenGL by going over to LearnOpenGL.com!". And they teach 3.3 Core. If you don't yet know about OpenGL versions, that's the oldest version of the "newer" set of OpenGL versions. If you do know enough about OpenGL to have an opinion on what other version to use, you could also use any other version as well.

LoadLibraryW

On both MSDN and the OpenGL Wiki it says that any function that's in OpenGL32.dll is not able to be looked up with wglGetProcAddress. Instead you have to use the GetProcAddress function. To use that, we need to have a loaded library. The library loading itself uses a textual name, so it has A and W versions. As usual, we want the W version, so we want LoadLibraryW. When we're all done with the library we'll use FreeLibrary to close out the library. The FreeLibrary call just takes the handle to the module, so it doesn't have A and W variants.


#![allow(unused)]
fn main() {
/// Pointer to a procedure of unknown type.
pub type FARPROC = *mut c_void;

#[link(name = "Kernel32")]
extern "system" {
  /// [`LoadLibraryW`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw)
  pub fn LoadLibraryW(lpLibFileName: LPCWSTR) -> HMODULE;

  /// [`FreeLibrary`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary)
  pub fn FreeLibrary(hLibModule: HMODULE) -> BOOL;

  /// [`GetProcAddress`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress)
  pub fn GetProcAddress(hModule: HMODULE, lpProcName: LPCSTR) -> FARPROC;
}

/// Loads a dynamic library.
///
/// The precise details of how the library is searched for depend on the input
/// string.
///
/// See [`LoadLibraryW`](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw)
pub fn load_library(name: &str) -> Result<HMODULE, Win32Error> {
  let name_null = wide_null(name);
  // Safety: the input pointer is to a null-terminated string
  let hmodule = unsafe { LoadLibraryW(name_null.as_ptr()) };
  if hmodule.is_null() {
    Err(get_last_error())
  } else {
    Ok(hmodule)
  }
}
}

Also, if you're wondering why GetProcAddress doesn't have A and W versions, it's because C function names can only ever be ANSI content.

Now we can put an HMODULE into our WindowData struct.


#![allow(unused)]
fn main() {
struct WindowData {
  hdc: HDC,
  hglrc: HGLRC,
  opengl32: HMODULE,
}
}

Then we can load a module and assign it. We can do this basically anywhere in the startup process, but it's emotionally connected to using GL, so we'll do it right after we make our context.


#![allow(unused)]
fn main() {
  unsafe { wgl_make_current(hdc, hglrc) }.unwrap();
  unsafe { (*lparam).hglrc = hglrc };

  let opengl32 = load_library("opengl32.dll").unwrap();
  unsafe { (*lparam).opengl32 = opengl32 };
}

And we have to properly close out the module when we're cleaning up the window.


#![allow(unused)]
fn main() {
    WM_DESTROY => {
      println!("WM_DESTROY");
      match get_window_userdata::<WindowData>(hwnd) {
        Ok(ptr) if !ptr.is_null() => {
          let window_data = Box::from_raw(ptr);
          FreeLibrary(window_data.opengl32);
          wgl_delete_context(window_data.hglrc)
            .unwrap_or_else(|e| eprintln!("GL Context deletion error: {}", e));
          // ...
}

What Do We Load?

Now that we can load up GL functions, what do we want to load? And what are the type signatures?

Well, for that, we could look at our old friend gl.xml. It describes the entire GL API in a structured way.

However, that's overkill for what we need at the moment. We only need to use like two functions to just clear the screen to a color, so instead we'll just check the online manual pages. What we're after for is glClearColor and also glClear.

What's a GLfloat and a GLbitfield? For that we can look in gl.xml. If we look around we'll eventually find these entries:

<type>typedef unsigned int <name>GLbitfield</name>;</type>

<type requires="khrplatform">typedef khronos_float_t <name>GLfloat</name>;</type>

Cool. Hmm, we need a new library module for this. These definitions will be common to our GL usage across all the platforms, so let's start a new file for that.


#![allow(unused)]
fn main() {
// lib.rs

//! Library for the [Triangle From Scratch][tfs] project.
//!
//! [tfs]: https://rust-tutorials.github.io/triangle-from-scratch/

mod macros;

pub mod util;

#[cfg(windows)]
pub mod win32;
// this is so that gl will see the C types
#[cfg(windows)]
use win32::*;

pub mod gl;
}

And then our fun new module


#![allow(unused)]
#![allow(non_camel_case_types)]

fn main() {
use super::*;

/// From `gl.xml`
pub type GLbitfield = c_uint;

/// From `gl.xml`
pub type GLfloat = c_float;

/// See [`glClearColor`](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glClearColor.xhtml)
pub type glClearColor_t = Option<
  unsafe extern "system" fn(
    red: GLfloat,
    green: GLfloat,
    blue: GLfloat,
    alpha: GLfloat,
  ),
>;

/// See [`glClear`](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glClear.xhtml)
pub type glClear_t = Option<unsafe extern "system" fn(mask: GLbitfield)>;
}

Hmm, but where do we get the values for the GL_COLOR_BUFFER_BIT and so on? That's also stored in gl.xml.

If you search for GL_COLOR_BUFFER_BIT you'll see a lot of "group" info, but that's old and not what we want. Eventually if you keep looking you'll see a line like this:

<enum value="0x00004000" name="GL_COLOR_BUFFER_BIT" group="ClearBufferMask,AttribMask"/>

This is good, this is good.

So what are the groups? Well, there's been a heroic effort by the GL maintainers to get the gl.xml descriptions of functions to be slightly better documented by giving each function input a group, and then each constant lists what groups it's in.

If we look at the XML definition for glClear:

<command>
  <proto>void <name>glClear</name></proto>
  <param group="ClearBufferMask"><ptype>GLbitfield</ptype> <name>mask</name></param>
  <glx type="render" opcode="127"/>
</command>

See, that mask argument should be a constant in the "ClearBufferMask" group. And GL_COLOR_BUFFER_BIT is in the "ClearBufferMask" group, so it would be a correct call to make.

This is just some info to try and help static checkers, but it's still pretty loosey-goosey, and you don't really have to pay much attention if you don't care to. We won't be following the group info while we're doing this by hand. If we make a fancy generator then that might care to track the group info.

So we add some fields to our window data:


#![allow(unused)]
fn main() {
struct WindowData {
  hdc: HDC,
  hglrc: HGLRC,
  opengl32: HMODULE,
  gl_clear: glClear_t,
  gl_clear_color: glClearColor_t,
}
}

Then we add some functions to our window data:


#![allow(unused)]
fn main() {
impl WindowData {
  pub fn gl_get_proc_address(&self, name: &[u8]) -> *mut c_void {
    assert!(*name.last().unwrap() == 0);
    let p = unsafe { wglGetProcAddress(name.as_ptr().cast()) };
    match p as usize {
      0 | 1 | 2 | 3 | usize::MAX => unsafe {
        GetProcAddress(self.opengl32, name.as_ptr().cast())
      },
      _ => p,
    }
  }
  #[rustfmt::skip]
  pub unsafe fn load_gl_functions(&mut self) {
    self.gl_clear = core::mem::transmute(self.gl_get_proc_address(c_str!("glClear")));
    self.gl_clear_color = core::mem::transmute(self.gl_get_proc_address(c_str!("glClearColor")));
  }
}
}

And then, in addition to simply setting the loaded library, we also tell the window data to do its loading process:


#![allow(unused)]
fn main() {
  let opengl32 = load_library("opengl32.dll").unwrap();
  unsafe { (*lparam).opengl32 = opengl32 };
  unsafe { (*lparam).load_gl_functions() };
}

Clear The Screen

To clear the screen's color we call glClear(GL_COLOR_BUFFER_BIT). We can also set the color we want to clear things to. By default it'll clear to black, but we can select any color we want.


#![allow(unused)]
fn main() {
    WM_PAINT => match get_window_userdata::<WindowData>(hwnd) {
      Ok(ptr) if !ptr.is_null() => {
        let window_data = ptr.as_mut().unwrap();
        (window_data.gl_clear_color.unwrap())(0.6, 0.7, 0.8, 1.0);
        (window_data.gl_clear.unwrap())(GL_COLOR_BUFFER_BIT);
      }
}

And there's one more step!

We need to call SwapBuffers on our HDC to tell windows to swap the front and back buffers.

Declare it.


#![allow(unused)]
fn main() {
// in the library's win32.rs

#[link(name = "Gdi32")]
extern "system" {
  /// [`SwapBuffers`](https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-swapbuffers)
  pub fn SwapBuffers(Arg1: HDC) -> BOOL;
}
}

And then call it.


#![allow(unused)]
fn main() {
    WM_PAINT => match get_window_userdata::<WindowData>(hwnd) {
      Ok(ptr) if !ptr.is_null() => {
        let window_data = ptr.as_mut().unwrap();
        (window_data.gl_clear_color.unwrap())(0.6, 0.7, 0.8, 1.0);
        (window_data.gl_clear.unwrap())(GL_COLOR_BUFFER_BIT);
        SwapBuffers(window_data.hdc);
      }
}

And we finally get a nice, soft, blue sort of color in our window.

Swap Interval

Oh heck, we didn't set a swap interval!

Remember how we loaded up a pointer for wglSwapIntervalEXT, and then we didn't use it at all? Uh, I guess we can call it after we load the GL functions. We just need to set it once and it'll stay that way for the rest of the program.


#![allow(unused)]
fn main() {
  let opengl32 = load_library("opengl32.dll").unwrap();
  unsafe { (*lparam).opengl32 = opengl32 };
  unsafe { (*lparam).load_gl_functions() };

  // Enable "adaptive" vsync if possible, otherwise normal vsync
  if wgl_extensions.iter().any(|s| s == "WGL_EXT_swap_control_tear") {
    unsafe { (wglSwapIntervalEXT.unwrap())(-1) };
  } else {
    unsafe { (wglSwapIntervalEXT.unwrap())(1) };
  }
}

Now, any time we call SwapBuffers, the system will sync the swap with the vertical trace of the screen, and it'll wait at least 1 full monitor cycle between each swap.

If we have the adaptive vsync available, it'll still wait at least 1 frame, but if we're only slightly off from the correct time, it'll swap immediately. This reduces visual stutter by allowing occasional visual tearing. Neither of those are great, but sometimes the program will struggle to keep up. Usually the vsync mode is a user setting within a game or whatever, so you can just let users pick how they want to handle things.

Are We Done?

Yeah, basically!

You understand the basics of how we find a GL function type, lookup the function to load it into the program at runtime, and call it to make something happen.

and now you can learn how to do the rest of OpenGL by going over to LearnOpenGL.com!

I promised I'd say that to you one day.

No, but really, the basics have all been explained. There's a lot of stuff that's still clunky as heck, but it all works.

What's certainly left to do is make it more ergonomic. However, we're already at just over 9300 words. We might talk about ways to make GL nice to work with in another lesson some day.

Web Nonsense

People really like to run stuff in the browser. It's very nice to end users if they can just open a web page and not have to install a whole thing.

If you want to run Rust code in a browser you compile it to WebAssembly, or WASM for short, which is an output target the same as compiling for windows x86_64, or linux arm, or any other target.

Even then, Wasm is strongly sandboxed, and it cannot directly interact with the world. Not only do you have to bind to some external functions on the Rust side, you have to write those external functions yourself in javascript. This is a bit of a bother, and so, for this one target platform, we'll first see how to do it ourselves, and then we'll see how to leverage the most common crate for targeting wasm.

Web GL with bare Wasm

I should give a big thanks to kettle11, who made the hello_triangle_wasm_rust example for me.

Also, I should probably have an extra reminder at the top of this lesson: This is the "doing it all yourself" style. Much of the "Rust for Wasm" ecosystem uses a crate called wasm-bindgen. In the same way that, if you "just want to open a window" you would often reach for winit or sdl2 or something, if you "just want to show something in the browser" you'll often use wasm-bindgen (and the crates that go with it). People will at least expect that you're using wasm-bindgen if you get lost and need to ask someone for help. They've got a book of their own, with many many examples, so have a look there if that's what you wanna do.

Toolchain Setup

Before we even begin, we'll need to take a few extra steps to have the right compiler and tools available.

In addition to having Rust installed, we need to install the wasm32-unknown-unknown target:

rustup target add wasm32-unknown-unknown

In addition, you may wish to obtain the wasm-opt tool from their GitHub repo, though it's not required.

You also might wish to obtain the wasm-strip tool from The WebAssembly Binary Toolkit (WABT). It lets you strip debugging symbols and such from the program, reducing the size by quite a bit. You can also do this without an extra tool via a Nightly rustc flag.

Once you've compiled your program to wasm you'll also need some way to display it.

Unfortunately, you can't simply open a local file in your browser using a file:// address.

This is fine for a plain HTML file, but browsers (rightly) get more paranoid every day, so they don't support wasm execution in pages loaded through a file address. If you don't already have such a thing (I didn't), then you can try devserver.

cargo install devserver

If you already have your own favorite way to spin up a local server that can serve static files, that's fine too.

Separate Folder

This will have a few non-standard bits of setup, so I'm going to put it in a web_crate/ directory.

First it needs its own Cargo.toml file:

[package]
name = "triangle-from-scratch-web-crate"
version = "0.1.0"
authors = ["Lokathor <zefria@gmail.com>"]
edition = "2018"
license = "Zlib OR Apache-2.0 OR MIT"

And to make a wasm library we need to tell Rust that the crate-type will be cdylib:

[lib]
crate-type = ["cdylib"]

Personally I also like to turn on link-time optimization with release builds, not because it's required, but just because I'm willing to spend some extra compile time to get a performance edge. The winner here is "thin", which provides almost all the benefit for a minimal amount of additional time and memory taken to compile.

[profile.release]
lto = "thin"

Now we're set.

The Wasm Library

As you can sorta already see, our "program" isn't actually going to be built as an executable. Instead, it's going to be built as a C-compatible library that the JavaScript of the webpage will load and use. This means that instead of writing a main.rs with an optional lib.rs, we put 100% of the code into lib.rs right from the start.


#![allow(unused)]
fn main() {
// lib.rs

#[no_mangle]
pub extern "C" fn start() {
  // nothing yet!
}
}

Note the use of the no_mangle attribute. This totally disables the usual name mangling that Rust does. It allows for the function to be called by external code that doesn't know Rust's special naming scheme, which is good, but there can only be a single function with a given name anywhere. In other words, if some other function named start with no mangling existed anywhere in our project, or in any of our dependencies, then we'd get a compilation error. That's why name mangling is on by default.

Also note that we have to declare that our start function uses the extern "C" ABI, this will give us the correct calling convention when communicating between JavaScript and Wasm.

When JavaScript loads up our wasm module, the start function will be called. This will allow our program to do whatever it wants to do, similar to the main function in a normal program.

The Web Page

Okay now we need a webpage for the user to display and have the wasm go.

I'm absolutely not a web development person, but I know just enough to throw some HTML together by hand:

<html>

<body>
  <canvas width="800" height="600" id="my_canvas"></canvas>
</body>

</html>

Next we start the local server and go to the page.

D:\dev\triangle-from-scratch>cd web_crate

D:\dev\triangle-from-scratch\web_crate>devserver

Serving [D:\dev\triangle-from-scratch\web_crate\] at [ https://localhost:8080 ] or [ http://localhost:8080 ]
Automatic reloading is enabled!
Stop with Ctrl+C

And it says "Hello." in the middle of the page. We'll just leave that open in one console and it'll automatically reload files as necessary.

Now we build our wasm module (note the --target argument):

D:\dev\triangle-from-scratch\web_crate>cargo build --release --target wasm32-unknown-unknown
   Compiling triangle-from-scratch-web-crate v0.1.0 (D:\dev\triangle-from-scratch\web_crate)
    Finished release [optimized] target(s) in 1.16s

which makes a file: target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm

(If we hadn't used the --release flag, then it'd be in target/wasm32-unknown-unknown/debug/ instead.)

Now we have to alter our page to load the wasm via a script:

<html>

<body>
  <canvas width="800" height="600" id="my_canvas"></canvas>
  <script>
    var importObject = {};

    const mod_path = 'target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm';
    WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
      .then(results => {
        results.instance.exports.start();
      });
  </script>
</body>

</html>

What's going on here? Well, you should sure read the Loading and running WebAssembly code tutorial on the Mozilla Developer Network (MDN) page.

  • First we call WebAssembly.instantiateStreaming()
    • The first argument is whatever will give us the wasm stream. In this case, a call to fetch.
    • The second argument is the "import object", which lets us provide things to the wasm code. At the moment we don't provide anything to the wasm, so we use an empty object.
  • This gives a Promise<ResultObject>, so we use the then method to do something to the results. It's similar to Rust's async/await and Future stuff. Except it's not quite the same, they tell me. I don't really know JavaScript, but I'm kinda just nodding and smiling as we go.
  • When acting on the results, results.module is the web assembly module and results.instance is the web assembly instance. The module isn't too helpful to us right now, but by using the instance we can call our start function (or any other non-mangled public function).

Make The Wasm Do Something

It's not too exciting for nothing to happen. Let's have the wasm clear the canvas to a non-white color.

First we expand the script on the web page. What we need to do is give the wasm code some functions to let it interact with the outside world.

  <script>
    var gl;
    var canvas;

    function setupCanvas() {
      console.log("Setting up the canvas...");
      let canvas = document.getElementById("my_canvas");
      gl = canvas.getContext("webgl");
      if (!gl) {
        console.log("Failed to get a WebGL context for the canvas!");
        return;
      }
    }

    function clearToBlue() {
      gl.clearColor(0.1, 0.1, 0.9, 1.0);
      gl.clear(gl.COLOR_BUFFER_BIT);
    }

    var importObject = {
      env: {
        setupCanvas: setupCanvas,
        clearToBlue: clearToBlue,
      }
    };

    const mod_path = 'target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm';
    WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
      .then(results => {
        results.instance.exports.start();
      });
  </script>

Now our importObject has an env field. Each function declared in here will be accessible to the wasm as an external function. One of them sets up the canvas and WebGL context. The other clears the canvas to a nice blue color.

Now we can call these from the Rust code:


#![allow(unused)]
fn main() {
mod js {
  extern "C" {
    pub fn setupCanvas();
    pub fn clearToBlue();
  }
}

#[no_mangle]
pub extern "C" fn start() {
  unsafe {
    js::setupCanvas();
    js::clearToBlue();
  }
}
}

And we'll see a blue canvas!

Note that JavaScript convention doesn't use snake_case naming, they use camelCase naming. The naming style isn't significant to the compiler, it's just a convention.

Workflow Tweaks

When we want to rebuild our wasm module we have to use the whole cargo build --release --target wasm32-unknown-unknown each time. Horrible. Let's make a .cargo/config.toml file in our web_stuff crate folder. Then we can set the default build target to be for wasm:

[build]
target = "wasm32-unknown-unknown"

Now cargo build and cargo build --release will pick the wasm32-unknown-unknown target by default.

Also, here is where we can easily pass the flag for rustc to strip the symbols from the output:

[build]
target = "wasm32-unknown-unknown"
rustflags = ["-Zstrip=symbols"]

The -Z part means that it's an unstable flag, so we can only do it with Nightly. If you want to strip the symbols but stick to Stable Rust you'll have to get the wasm-strip tool from the wabt toolkit that I mentioned before. Stripping the symbols just makes the output smaller, so there's less to send over the network. In a small example like ours, it changes the final output size from 308 bytes to 161 bytes. Our code isn't doing too much, in terms of instructions, so just putting in the debug symbols is a hefty percentage of the overall bytes taken. We'll have another look when our program is doing a bit more to see if it's still a big difference.

Also, it's a little annoying to have to manually rebuild our wasm when the HTML pages reloads automatically. To fix this, we can get cargo-watch

cargo install cargo-watch

And then run a cargo-watch instance to automatically rebuild the code as necessary:

cargo watch -c -x "build --release"

The -c clears the terminal each time the watch restarts so that you never look at old output by accident.

The -x "build --release" executes "cargo build --release" each time cargo-watch detects a change.

Now we will always have both the latest HTML and wasm in our browser page.

Drawing A Triangle

We need a little more wasm/js interaction than what we have right now to actually draw a triangle.

If you want a larger WebGL tutorial you should check out the one on MDN. WebGL is based on OpenGL ES 2.0, which is based on OpenGL 2.0, so if you already know about GL stuff, this will probably look very familiar.

For now, we'll mostly skip over the WebGL explanations themselves. Instead we'll focus on the interoperation guts that let our wasm interact with WebGL.

The Rust Code

What we want is for our rust code to look something like this:


#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn start() {
  unsafe {
    js::setupCanvas();

    let vertex_data = [-0.2_f32, 0.5, 0.0, -0.5, -0.4, 0.0, 0.5, -0.1, 0.0];
    let vertex_buffer = js::createBuffer();
    js::bindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
    js::bufferDataF32(
      GL_ARRAY_BUFFER,
      vertex_data.as_ptr(),
      vertex_data.len(),
      GL_STATIC_DRAW,
    );

    let index_data = [0_u16, 1, 2];
    let index_buffer = js::createBuffer();
    js::bindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer);
    js::bufferDataU16(
      GL_ELEMENT_ARRAY_BUFFER,
      index_data.as_ptr(),
      index_data.len(),
      GL_STATIC_DRAW,
    );

    let vertex_shader_text = "
      attribute vec3 vertex_position;
      void main(void) {
        gl_Position = vec4(vertex_position, 1.0);
      }";
    let vertex_shader = js::createShader(GL_VERTEX_SHADER);
    js::shaderSource(
      vertex_shader,
      vertex_shader_text.as_bytes().as_ptr(),
      vertex_shader_text.len(),
    );
    js::compileShader(vertex_shader);

    let fragment_shader_text = "
      void main() {
        gl_FragColor = vec4(1.0, 0.5, 0.313, 1.0);
      }";
    let fragment_shader = js::createShader(GL_FRAGMENT_SHADER);
    js::shaderSource(
      fragment_shader,
      fragment_shader_text.as_bytes().as_ptr(),
      fragment_shader_text.len(),
    );
    js::compileShader(fragment_shader);

    let shader_program = js::createProgram();
    js::attachShader(shader_program, vertex_shader);
    js::attachShader(shader_program, fragment_shader);
    js::linkProgram(shader_program);
    js::useProgram(shader_program);

    let name = "vertex_position";
    let attrib_location = js::getAttribLocation(
      shader_program,
      name.as_bytes().as_ptr(),
      name.len(),
    );
    assert!(attrib_location != GLuint::MAX);
    js::enableVertexAttribArray(attrib_location);
    js::vertexAttribPointer(attrib_location, 3, GL_FLOAT, false, 0, 0);

    js::clearColor(0.37, 0.31, 0.86, 1.0);
    js::clear(GL_COLOR_BUFFER_BIT);
    js::drawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0);
  }
}
}

I don't want to cover too many details of how WebGL works right now because we're mostly focusing on the Wasm stuff, but here are the broad steps:

  • Initialize the canvas
  • Bind a buffer as the ARRAY_BUFFER and then place our vertex data into it.
  • Bind a buffer as the ELEMENT_ARRAY_BUFFER and then give it our index data.
  • Create a vertex shader
  • Create a fragment shader
  • Create a program, connect the two shaders, then link, then use.
  • Get the location of the vertex_position attribute, enable that location, and then point the location at the correct position within our vertex array.
  • Clear the screen to our background color.
  • Draw the triangle.

If you're used to OpenGL, or even to graphics programming using some other API, this should all feel quite familiar.

To support our start function we need to have quite a few more extern declarations, and also const declarations:


#![allow(unused)]
fn main() {
pub type GLenum = u32;
pub type GLbitmask = u32;
pub type GLuint = u32;
pub type GLint = i32;
pub type GLsizei = i32;
// Note(kettle11): GLintptr should be an i64, but those can't be properly passed
// between Wasm and Javascript, so for now just use an i32.
pub type GLintptr = i32;

#[derive(Clone, Copy)]
#[repr(C)]
pub struct JSObject(u32);
impl JSObject {
  pub const fn null() -> Self {
    JSObject(0)
  }
}

use constants::*;
mod constants {
  //! Values taken from the [WebGL Constants page](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants).
  //!
  //! All names here have the `GL_` prefix added.

  use super::{GLbitmask, GLenum};

  pub const GL_ARRAY_BUFFER: GLenum = 0x8892;
  pub const GL_ELEMENT_ARRAY_BUFFER: GLenum = 0x8893;
  pub const GL_FLOAT: GLenum = 0x1406;
  pub const GL_FRAGMENT_SHADER: GLenum = 0x8B30;
  pub const GL_STATIC_DRAW: GLenum = 0x88E4;
  pub const GL_TRIANGLES: GLenum = 0x0004;
  pub const GL_UNSIGNED_SHORT: GLenum = 0x1403;
  pub const GL_VERTEX_SHADER: GLenum = 0x8B31;

  pub const GL_COLOR_BUFFER_BIT: GLbitmask = 0x00004000;
}

mod js {
  //! Holds our `extern "C"` declarations for javascript interactions.

  use super::*;

  extern "C" {
    pub fn setupCanvas();

    //

    pub fn attachShader(program: JSObject, shader: JSObject);
    pub fn bindBuffer(target: GLenum, id: JSObject);
    pub fn bufferDataF32(
      target: GLenum, data_ptr: *const f32, data_length: usize, usage: GLenum,
    );
    pub fn bufferDataU16(
      target: GLenum, data_ptr: *const u16, data_length: usize, usage: GLenum,
    );
    pub fn clear(mask: GLbitmask);
    pub fn clearColor(r: f32, g: f32, b: f32, a: f32);
    pub fn compileShader(program: JSObject);
    pub fn createBuffer() -> JSObject;
    pub fn createProgram() -> JSObject;
    pub fn createShader(shader_type: GLenum) -> JSObject;
    pub fn drawElements(
      mode: GLenum, count: GLsizei, type_: GLenum, offset: GLintptr,
    );
    pub fn enableVertexAttribArray(index: GLuint);
    pub fn getAttribLocation(
      program: JSObject, name: *const u8, name_length: usize,
    ) -> GLuint;
    pub fn linkProgram(program: JSObject);
    pub fn shaderSource(
      shader: JSObject, source: *const u8, source_length: usize,
    );
    pub fn useProgram(program: JSObject);
    pub fn vertexAttribPointer(
      index: GLuint, size: GLint, type_: GLenum, normalized: bool,
      stride: GLsizei, pointer: GLintptr,
    );
  }
}
}

This is pretty normal stuff, except the JsObject thing. What's going on there?

Well, we can't pass a whole javascript object over the C FFI. What even is a javascript object, anyway? I dunno, some sort of hash... thing... with fields. It doesn't matter. The point is that it's a type that you can't pass over the C FFI. That's mostly fine, except that we need to communicate with GL about them.

What we'll do is store all our javascript objects in a list out in javascript-land, and then in the WASM we just use the index values into that list to name the javascript objects when we need to.

The JavaScript Code

On the javascript side of things, we mostly add a bunch of boring functions, but a few are interesting.

First we set up a few more variables we'll use. We have the gl and canvas from before, but now we'll need to make the javascript and wasm memory interact, and we'll also need to track javascript objects that the wasm knows about. Since we need to transfer strings between wasm and javascript, we'll need a TextDecoder.

<html>

<body>
  <canvas width="800" height="600" id="my_canvas"></canvas>
  <script>
    var gl;
    var canvas;
    var wasm_memory;
    var js_objects = [null];

    const decoder = new TextDecoder();

The canvas setup is basically the same as before,

    function setupCanvas() {
      console.log("Setting up the canvas.");
      let canvas = document.getElementById("my_canvas");
      gl = canvas.getContext("webgl");
      if (!gl) {
        console.log("Failed to get a WebGL context for the canvas!");
        return;
      }
    }

But making our importObject is quite a few functions this time:

    var importObject = {
      env: {
        setupCanvas: setupCanvas,

        attachShader: function (program, shader) {
          gl.attachShader(js_objects[program], js_objects[shader]);
        },
        bindBuffer: function (target, id) {
          gl.bindBuffer(target, js_objects[id]);
        },
        bufferDataF32: function (target, data_ptr, data_length, usage) {
          const data = new Float32Array(wasm_memory.buffer, data_ptr, data_length);
          gl.bufferData(target, data, usage);
        },
        bufferDataU16: function (target, data_ptr, data_length, usage) {
          const data = new Uint16Array(wasm_memory.buffer, data_ptr, data_length);
          gl.bufferData(target, data, usage);
        },
        clear: function (mask) {
          gl.clear(mask)
        },
        clearColor: function (r, g, b, a) {
          gl.clearColor(r, g, b, a);
        },
        compileShader: function (shader) {
          gl.compileShader(js_objects[shader]);
        },
        createBuffer: function () {
          return js_objects.push(gl.createBuffer()) - 1;
        },
        createProgram: function () {
          return js_objects.push(gl.createProgram()) - 1;
        },
        createShader: function (shader_type) {
          return js_objects.push(gl.createShader(shader_type)) - 1;
        },
        drawElements: function (mode, count, type, offset) {
          gl.drawElements(mode, count, type, offset);
        },
        enableVertexAttribArray: function (index) {
          gl.enableVertexAttribArray(index)
        },
        getAttribLocation: function (program, pointer, length) {
          const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
          const string = decoder.decode(string_data);
          return gl.getAttribLocation(js_objects[program], string);
        },
        linkProgram: function (program) {
          gl.linkProgram(js_objects[program]);
        },
        shaderSource: function (shader, pointer, length) {
          const string_data = new Uint8Array(wasm_memory.buffer, pointer, length);
          const string = decoder.decode(string_data);
          gl.shaderSource(js_objects[shader], string);
        },
        useProgram: function (program) {
          gl.useProgram(js_objects[program]);
        },
        vertexAttribPointer: function (index, size, type, normalized, stride, offset) {
          gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
        },
      }
    };

Slices

Of note is this function:

bufferDataF32: function (target, data_ptr, data_length, usage) {
  const data = new Float32Array(wasm_memory.buffer, data_ptr, data_length);
  gl.bufferData(target, data, usage);
},

To access a memory slice living in the wasm memory, first the wasm code passes a pointer and length out to javascript. Then the javascript uses that to make an array object. In this case, a Float32Array. We have a similar function for u16 as well.

Strings take an extra step: once you've made a Uint8Array we have to use our decoder to convert that into javascript's natural string type.

Objects

When we need to get an object from WebGL and pass it along to wasm, we just push it into our list and tell wasm the index of the object. The push method on arrays returns the new length, so we just subtract 1 and that'll be the index of the newly added object.

createBuffer: function () {
  return js_objects.push(gl.createBuffer()) - 1;
},

This is not the most extensible system. We can't ever delete and objects with this basic setup. If we remove an element from our list, all the slots after would have the wrong index.

To allow for deletion, we'd need to change deleted elements to null, and then when a new object is requested we'd scan the list looking for the first null (other than at index 0), and put the object at that position. If we don't find any open spots in the list, only then do we push it onto the end of the list.

Alternately, we could store all of our objects in a Map. This would let us simply assign an object to a key, and when we're done with it we'd delete the key.

I'm not a javascript expert, in fact I'm barely a javascript beginner, so I don't know which of those would be better in the long term.

Any time we need to let the wasm "use" an object, it passes the index of the object and we look it up from our list:

attachShader: function (program, shader) {
  gl.attachShader(js_objects[program], js_objects[shader]);
},

Wasm Startup

Finally, we need to change one more thing about the startup code.

After we get the results back, we have to assign the exported memory to our wasm_memory value. This lets all the other functions manipulate is when they need to.

const mod_path = 'target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm';
WebAssembly.instantiateStreaming(fetch(mod_path), importObject)
  .then(results => {
    console.log("Wasm instance created.");
    // assign the memory to be usable by the other functions
    wasm_memory = results.instance.exports.memory;
    // start the wasm
    results.instance.exports.start();
  });

And Now There's A Triangle!

We've finally got our triangle on the page!

A static image isn't the best, so in the next lesson we'll cover how to get some user interaction.

Appendix

This part of the book has articles on stuff that's even more of a yak shave than normal.

It's useful, but it's just a diversion from our main focus.

(Note: Thanks to Plecra for helping with the code for this article.)

UTF-16 Literals

Rust literals are textual content encoded as UTF-8.

Sometimes we want our textual content in other encodings instead.

This can be done at runtime, but why do at runtime what you could be doing at compile time?

What is Unicode?

Unicode is a huge pile of nonsense, that's what it is.

There's an old article from 2003 called The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!), and the title of that article is correct.

You should stop and read it if you haven't, and from here on I'm going to assume you've read it.

const fn

In Rust, if you want to do something at compile time you must use a const fn. However, Rust const fn is only partly implemented within the language. There is much that is not yet done, and most importantly const fn doesn't support traits.

Not having trait support means that we have to do things a little weird. For one, we can't use normal iterators. Another, we can't use string slice indexing.

So we'll have to write some const fn stuff without using traits.

break_off_code_point

First, we want a const fn way to break a code point off the front of a utf-8 byte slice. Normally, we'd use str::chars, but remember that there's no iterators here. We'll have to just do it ourselves.

It's not too difficult. We just follow the spec, as described on the the wikipedia for utf-8.

First we need a function that will take in some utf8 bytes, break off one code point worth of bytes, and then return what code point it found and the rest of the bytes. All of this might fail (such as if the input is an empty slice), so the output is an Option


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  None
}
}

As I mentioned above, we can't sub-slice a &str value in a const context in current rust, so we'll have to deal directly in &[u8] for now.

Determine the next code point's byte count

The first thing we do is decide how many bytes we're going to use from the input.

As a "default" case, if we can't find a code point in the input we'll give none.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [..] => None
  }
}
}

To determine how many bytes we'll use up to get our code point we look at the bits of the leading byte. For any multi-byte sequence the number of initial 1 bits is the number of total bytes in the sequence.

  • If the initial bit is 0, then that's a one byte sequence.
  • If the initial bits are 110, then that's a two byte sequence.
  • If the initial bits are 1110, then that's a three byte sequence.
  • If the initial bits are 11110, then that's a four byte sequence.

We can look at the initial byte and see what case we're in using a slice pattern.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [0b00000000..=0b01111111, ..] => None, /* one */
    [0b11000000..=0b11011111, ..] => None, /* two */
    [0b11100000..=0b11101111, ..] => None, /* three */
    [0b11110000..=0b11110111, ..] => None, /* four */
    [..] => None,                          /* default */
  }
}
}

Except that after checking the bit pattern we need to store that byte, because we'll need it to determine the output. Also we'll need to use the "rest of the bytes" too. For this we can add an identifier pattern to the matching segments.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [a @ 0b00000000..=0b01111111, rest @ ..] => None, /* one */
    [a @ 0b11000000..=0b11011111, rest @ ..] => None, /* two */
    [a @ 0b11100000..=0b11101111, rest @ ..] => None, /* three */
    [a @ 0b11110000..=0b11110111, rest @ ..] => None, /* four */
    [..] => None,                                     /* default */
  }
}
}

Slice patterns and identifier patterns are not very common in Rust, so if you're unfamiliar with them please go glance at the reference.

Okay, next adjustment is that the two, three, and four cases aren't actually pulling the right number of bytes off the slice.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [a @ 0b00000000..=0b01111111, rest @ ..] => None, /* one */
    [a @ 0b11000000..=0b11011111, b, rest @ ..] => None, /* two */
    [a @ 0b11100000..=0b11101111, b, c, rest @ ..] => None, /* three */
    [a @ 0b11110000..=0b11110111, b, c, d, rest @ ..] => None, /* four */
    [..] => None,                                     /* default */
  }
}
}

What's up with those comments? That's really how rustfmt puts it. Whatever.

Also note that technically the trailing bytes have their own limits on what's valid and what's not. We're going to take a page from Rust's book and ignore that. We'll simply assume that the trailing bytes in a multi-byte sequence are valid. If a caller gives us bad input, we might give them bad output back. There's not an actual safety concern with it, so it's not a big deal.

Compute the output code point

So now we need to fill in the output side of the four cases.

The one byte case is simple. We just return the value directly.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [a @ 0b00000000..=0b01111111, rest @ ..] => {
      // one byte
      Some((*a as u32, rest))
    }
    [a @ 0b11000000..=0b11011111, b, rest @ ..] => None, /* two */
    [a @ 0b11100000..=0b11101111, b, c, rest @ ..] => None, /* three */
    [a @ 0b11110000..=0b11110111, b, c, d, rest @ ..] => None, /* four */
    [..] => None,                                        /* default */
  }
}
}

The two byte case is where we start having to combine bits across different bytes. From the leading byte we take the lowest 5 bits, and from the trailing byte we take the lowest 6 bits.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [a @ 0b00000000..=0b01111111, rest @ ..] => {
      // one byte
      Some((*a as u32, rest))
    }
    [a @ 0b11000000..=0b11011111, b, rest @ ..] => {
      // two bytes
      let lead = (*a & 0b11111) as u32;
      let trail = (*b & 0b111111) as u32;
      Some((lead << 6 | trail, rest))
    }
    [a @ 0b11100000..=0b11101111, b, c, rest @ ..] => None, /* three */
    [a @ 0b11110000..=0b11110111, b, c, d, rest @ ..] => None, /* four */
    [..] => None,                                           /* default */
  }
}
}

The three and four byte cases are the same idea as the two byte case. The number of bits to use from the leading byte changes each time, but the number of bits to use from trailing bytes stays the same.


#![allow(unused)]
fn main() {
pub const fn break_off_code_point(utf8: &[u8]) -> Option<(u32, &[u8])> {
  match utf8 {
    [a @ 0b00000000..=0b01111111, rest @ ..] => {
      // one byte
      Some((*a as u32, rest))
    }
    [a @ 0b11000000..=0b11011111, b, rest @ ..] => {
      // two bytes
      let lead = (*a & 0b11111) as u32;
      let trail = (*b & 0b111111) as u32;
      Some((lead << 6 | trail, rest))
    }
    [a @ 0b11100000..=0b11101111, b, c, rest @ ..] => {
      // three bytes
      let lead = (*a & 0b1111) as u32;
      let trail1 = (*b & 0b111111) as u32;
      let trail2 = (*c & 0b111111) as u32;
      let out = lead << 12 | trail1 << 6 | trail2;
      Some((out, rest))
    }
    [a @ 0b11110000..=0b11110111, b, c, d, rest @ ..] => {
      // four bytes
      let lead = (*a & 0b111) as u32;
      let trail1 = (*b & 0b111111) as u32;
      let trail2 = (*c & 0b111111) as u32;
      let trail3 = (*d & 0b111111) as u32;
      let out = lead << 18 | trail1 << 12 | trail2 << 6 | trail3;
      Some((out, rest))
    }
    [..] => None, /* default */
  }
}
}

We can also write a small unit test for this:


#![allow(unused)]
fn main() {
#[test]
fn test_break_off_code_point() {
  // code points of 1, 2, 3, and 4 byte size
  for ch in &['$', '¢', 'ह', '€', '한', '𐍈'] {
    let s = format!("{}", ch);
    assert_eq!(break_off_code_point(s.as_bytes()), Some((*ch as u32, &[][..])));
  }

  // empty string works properly
  assert!(break_off_code_point("".as_bytes()).is_none());
}
}

A passing test doesn't conclusively prove that our function works, but it at least shows that the function does what we expect (as far as we tested).

Invalid Input

One thing we don't handle quite right is invalid input. Right now, our input is assumed to be correct. If the input doesn't match a case we expect, then we just give back None. If we're expecting to only process string literals with this, that's fine. However, we might want to process any input at some point, so let's do a little tweak to allow for lossy conversion of bad inputs.

All we have to do is break up the final [..] => None case into two cases.

  • An empty string goes to None
  • Our new "default" case gives the Unicode Replacement Character as the code point and consumes 1 byte if the current leading character is disallowed.

#![allow(unused)]
fn main() {
// in the `break_off_code_point` match
    [] => None,
    [_unknown, rest @ ..] => {
      // If we can't match anything above, we just pull off one byte and give
      // the unicode replacement code point.
      Some(('�' as u32, rest))
    }
}

This allows us to handle garbage in the middle of the input a little better.

It's still not perfectly conformant, because we've decided to skip on checking the trailing bytes for validity, but it's good enough in most cases that we'll make that trade.

count_utf16_code_units

Alright, so our goal was to re-encode utf-8 as utf-16. Now that we can iterate the code points of a utf-8 sequence, how are we going to build a utf-16 sequence?

First, we need to get an output buffer. To put our output. Since this is all in a const context, our output buffer is going to be an array. How big of an array do we need? Glad you asked.

Let's make another function for this:


#![allow(unused)]
fn main() {
pub const fn count_utf16_code_units(s: &str) -> usize {
  0
}
}

Off to a good start.

So we're going to walk the input string, and then for each code point we determine if it needs 1 or 2 code units to be stored. This will give us the capacity for how many u16 values our array will need to be.

The rule to count code units in utf-16 is simple: If the unicode code point value is less than or equal to 0xFFFF then it's 1 code unit in utf-16, otherwise it's 2 code units in utf-16.

We can write this with a simply while let loop, and we'll throw a unit test at it too.


#![allow(unused)]
fn main() {
pub const fn count_utf16_code_units(s: &str) -> usize {
  let mut bytes = s.as_bytes();
  let mut len = 0;
  while let Some((u, rest)) = break_off_code_point(bytes) {
    len += if u <= 0xFFFF { 1 } else { 2 };
    bytes = rest;
  }
  len
}

#[test]
fn test_count_utf16_code_units() {
  let s = "hello from the unit test";
  let normal_style: usize = s.chars().map(|ch| ch.len_utf16()).sum();
  assert_eq!(normal_style, count_utf16_code_units(s));

  let s = "$¢ह€한𐍈, 漢字, ひらがな / 平仮名, カタカナ / 片仮名";
  let normal_style: usize = s.chars().map(|ch| ch.len_utf16()).sum();
  assert_eq!(normal_style, count_utf16_code_units(s));
}
}

Cool, so now we can const count the number of code units we'll need to have.

Making A Macro

Okay, so now we want to input a string literal to something and get out a utf-16 literal.

Sadly, we can't do this with a const fn. The output type would depend on the input value. Rust doesn't like that idea at all.

Instead, we'll write a macro.


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! utf16 {
  // pass
}
}

Our macro has one match case it can do: you give it an expression and it'll process the text.


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! utf16 {
  ($text:expr) => {{
    todo!()
  }};
}
}

So when we get this $text value we want to assign it to a local const with a weird name. This helps avoid some const-eval issues you can potentially get like if the macro's caller has got an identifier in scope next to their macro usage that clashes with an identifier we're about to make in our macro. It sounds unlikely, but it did come up in real code when developing the crate that this article is based on. It doesn't really hurt, and it prevents a very cryptic error message from hitting the macro's user, so we'll do it.


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! utf16 {
  ($text:expr) => {{
    // Here we pick a name highly unlikely to exist in the scope
    // that $text came from, which prevents a potential const eval cycle error.
    const __A1B2C3D4_CONST_EVAL_LOOP_BREAK: &str = $text;
    const UTF8: &str = __A1B2C3D4_CONST_EVAL_LOOP_BREAK;
    todo!()
  }};
}
}

Next we'll make a const for the size of the output buffer.


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! utf16 {
  ($text:expr) => {{
    // Here we pick a name highly unlikely to exist in the scope
    // that $text came from, which prevents a potential const eval cycle error.
    const __A1B2C3D4_CONST_EVAL_LOOP_BREAK: &str = $text;
    const UTF8: &str = __A1B2C3D4_CONST_EVAL_LOOP_BREAK;
    const OUT_BUFFER_LEN: usize = $crate::util::count_utf16_code_units(UTF8);
    todo!()
  }};
}
}

Now we make a const for the output itself. It's an array with a length equal to OUT_BUFFER_LEN, but we need to mutate and fill it all in as we iterate the code points of the input, so we'll compute it using an inner block.

We start with a zeroed buffer of the right size, then we walk the input and write in each value. Because the normal encoding utilities in the core library aren't const fn, we have to do our own encoding math right here.


#![allow(unused)]
fn main() {
// ...
    const UTF16: [u16; OUT_BUFFER_LEN] = {
      let mut buffer = [0u16; OUT_BUFFER_LEN];
      let mut bytes = UTF8.as_bytes();
      let mut i = 0;
      while let Some((u, rest)) = $crate::util::break_off_code_point(bytes) {
        if u <= 0xFFFF {
          buffer[i] = u as u16;
          i += 1;
        } else {
          let code = u - 0x1_0000;
          buffer[i] = 0xD800 | ((code >> 10) as u16);
          buffer[i + 1] = 0xDC00 | ((code & 0x3FF) as u16);
          i += 2;
        }
        bytes = rest;
      }
      buffer
    };
// ...
}

Finally, we just return the whole array. In an initial version of this I had the output be just the data slice (&[u16]), but in some select situations you do need the data in an owned form, so the macro was adjusted to return the array directly. If you want it to be a slice, just prefix the call with &.


#![allow(unused)]
fn main() {
#[macro_export]
macro_rules! utf16 {
  ($text:expr) => {{
    // Here we pick a name highly unlikely to exist in the scope
    // that $text came from, which prevents a potential const eval cycle error.
    const __A1B2C3D4_CONST_EVAL_LOOP_BREAK: &str = $text;
    const UTF8: &str = __A1B2C3D4_CONST_EVAL_LOOP_BREAK;
    const OUT_BUFFER_LEN: usize = $crate::util::count_utf16_code_units(UTF8);
    const UTF16: [u16; OUT_BUFFER_LEN] = {
      let mut buffer = [0u16; OUT_BUFFER_LEN];
      let mut bytes = UTF8.as_bytes();
      let mut i = 0;
      while let Some((u, rest)) = $crate::util::break_off_code_point(bytes) {
        if u <= 0xFFFF {
          buffer[i] = u as u16;
          i += 1;
        } else {
          let code = u - 0x1_0000;
          buffer[i] = 0xD800 | ((code >> 10) as u16);
          buffer[i + 1] = 0xDC00 | ((code & 0x3FF) as u16);
          i += 2;
        }
        bytes = rest;
      }
      buffer
    };
    UTF16
  }};
}
}

And some handy tests wouldn't be out of place:


#![allow(unused)]
fn main() {
#[test]
fn test_utf16() {
  const HELLO16: [u16; 5] = utf16!("hello");
  assert_eq!(&HELLO16[..], &"hello".encode_utf16().collect::<Vec<u16>>());

  const WORDS8: &str = "$¢ह€한𐍈, 漢字, ひらがな / 平仮名, カタカナ / 片仮名";
  const WORDS16: &[u16] = &utf16!(WORDS8);
  assert_eq!(WORDS16, &WORDS8.encode_utf16().collect::<Vec<u16>>());
}
}

Ah, but often we want to have a null terminated string of utf-16. That's also no trouble at all:


#![allow(unused)]
fn main() {
/// As per [`utf16`], but places a null-terminator on the end.
#[macro_export]
macro_rules! utf16_null {
  ($text:expr) => {{
    const TEXT_NULL___A1B2C3D4: &str = concat!($text, '\0');
    $crate::utf16!(TEXT_NULL___A1B2C3D4)
  }};
}

#[test]
fn test_utf16_null() {
  const HELLO: &[u16] = &utf16_null!("hello");
  assert_eq!(HELLO, &"hello\0".encode_utf16().collect::<Vec<u16>>());
}
}

All very good, I hope you had fun with this one.

Crates

If you want this sort of thing at home there's already some crates to do it: