Kenny Kerr is a Software Engineer at Microsoft where he works on C++ and Rust tools and libraries for the Windows operating system. He is the creator of C++/WinRT and Rust for Windows. Kenny also wrote recurring columns and feature articles for C/C++ Users Journal, Visual Studio Magazine, MSDN Magazine, originally called Microsoft Systems Journal.

Getting Started with Rust

Kenny's courses on Pluralsight

Kenny on GitHub

Kenny on YouTube

Kenny on LinkedIn

Kenny's old blog on WordPress

Kenny's old blog on asp.net

Getting Started with Rust

The windows-rs project has been available for some time and while I still have a great deal of work left to do, I thought I should start spending some time writing about Rust for Windows and not simply building Rust for Windows. 😊 As I did for C++/WinRT, I thought I would start writing a few short "how to" or "how it works" articles to help developers understand some of the fundamentals of the windows-rs project.

Some of these topics will be obvious for Rust developers but perhaps not the Windows developer new to Rust. Other topics might be obvious to Windows developers but less so to the seasoned Rust developer new to Windows. Either way, I hope you find it useful. Feel free to open an issue on the repo if you have any questions.

Choosing between the windows and windows-sys crates

The windows crate provides bindings for the Windows API, including C-style APIs like CreateThreadpool as well as COM and WinRT APIs like DirectX. This crate provides the most comprehensive API coverage for the Windows operating system. Where possible, the windows crate also attempts to provide a more idiomatic and safe programming model for Rust developers.

The windows-sys crate provides raw bindings for the C-style Windows APIs. It lacks support for COM and WinRT APIs. The windows-sys crate was born out of the realization that the most expensive aspect of the windows crate, in terms of build time, is the cost of compiling function bodies. The Rust compiler just spends a great deal of effort compiling function bodies, so a version of the windows crate that only includes declarations is both much smaller and faster by comparison. The trouble is that COM-style virtual function calls require extra code gen in Rust (unlike C++) and this in turn leads to slower compile times. Enter the windows-sys crate.

Of course, we continue to work hard at improving performance both in terms of the underlying Rust compiler toolchain as well as the efficiency of the code generated for these crates. We are thus confident that the compile-time will continue to improve.

What do you need?windowswindows-sys
Fast compile times are one of your top concerns
You need no_std support
You need COM or WinRT support
You would prefer to use APIs that feel idiomatic to Rust
Minimum supported Rust version1.561.48

How are these crates built?

The windows and windows-sys crates are generated from metadata describing the Windows API. Originally only WinRT APIs included metadata, but metadata is now provided for older C and COM APIs as well. The win32metadata project provides the tools to produce the metadata and the windows-metadata and windows-bindgen crates are used to read the metadata and generate the windows and windows-sys crates. The bindings are generated differently based on the differing goals of the respective crates. You can find the exact metadata file used to generate a particular version of the windows and windows-sys crates here.

How do I find a particular API?

First pick the crate you would like to use. Then search the documentation for the chosen crate:

Note that the docs include a note indicating which features to enable in order to access a particular API.

What APIs are included?

All Windows APIs provided by the Windows SDK are included, with a few exceptions. The definitions of these APIs are collected from metadata and transformed into Rust bindings. The process of generating the Rust bindings purposefully omits a few APIs. APIs are only excluded if they are (1) unsuitable for Rust developers and (2) impose a large hit on the overall size of the windows and windows-sys crates.

The Xaml API is excluded because it is all but unusable without direct language support that only the Xaml team can provide. Xaml is also focused and tailored for C# app development so this API isn't applicable to Rust developers. The MsHtml API is also excluded because it is only intended for Microsoft's older scripting languages like JScript and VBScript. It is also by far the single largest module as measured in lines of code. Beyond that, a few deprecrated and unusable APIs are excluded. You can see exactly what the windows crate excludes and what the windows-sys crate excludes.

Beyond that, the windows-sys crate currently excludes all COM and WinRT APIs. The windows-sys crate only includes declarations and COM and WinRT calls are far too cumbersome without the abstractions provided by the windows crate. Here are some tips for choosing between the windows and windows-sys crates.

Where's my favorite macro from the Windows SDK?

The windows and windows-sys crates are generated from metadata. This metadata only includes type definitions and function signatures, not macros, header-only functions, or function bodies. You may find some equivalents of common C/C++ helper macros and functions in the windows crate, but in general the macros don't have direct equivalents in the windows or windows-sys crates.

Calling your first API with the windows crate

So you want to get a feel for calling a simple Windows API. Where to start? Let's look at a relatively simple API for submitting callbacks to the thread pool. You can read more about this API here.

The first step is to add a dependency on the windows crate and indicate which features you'd like to access:

[dependencies.windows]
version = "0.48"
features = [
    "Win32_Foundation",
    "Win32_System_Threading",
]

Why these two features? Well, the thread pool API is defined in the Win32::System::Threading module and we'll also use a handful of definitions from the Win32::Foundation module. If you're unsure, the docs for any given API provide a helpful comment indicating which features are required. For example, here are the docs for WaitForThreadpoolWorkCallbacks where you can see it depends on both of these features since it is defined in the Win32::System::Threading module and depends on BOOL which is defined in the Win32::Foundation module.

Cargo will now handle the heavy lifting, tracking down the dependencies and making sure the import libs are present, so that we can simply call these APIs in Rust without any further configuration. We can employ a use declaration to make these APIs a little more accessible:

#![allow(unused)]
fn main() {
use windows::{Win32::Foundation::*, Win32::System::Threading::*};
}

In order to "prove" that the code works and yet keep it real simple let's just use the thread pool to increment a counter some number of times. Here we can use a reader-writer lock for safe and multi-threaded access to the counter variable:

#![allow(unused)]
fn main() {
static COUNTER: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
}

For this example, I'll just use a simple main function with a big unsafe block since virtually everything here is going to be unsafe. Why is that? Well the windows crate lets you call foreign functions and these are generally assumed to be unsafe.

fn main() {
    unsafe {
        
    }
}

The thread pool API is modeled as a set of "objects" exposed via a traditional C-style API. The first thing we need to do is create a work object:

#![allow(unused)]
fn main() {
let work = CreateThreadpoolWork(Some(callback), None, None);
}

The first parameter is a pointer to a callback function. The remaining parameters are optional and you can read more about them in my thread pool series on MSDN.

Since this function allocates memory, it is possible that it might fail, and this is indicated by returning a null pointer rather than a valid work object handle. We'll check for this condition and call the GetLastError function to display any relevant error code:

#![allow(unused)]
fn main() {
if work.is_null() {
    println!("{:?}", GetLastError());
    return;
}
}

The callback itself must be a valid C-style callback according to the signature expected by the thread pool API. Here's a simple callback that will increment the count:

#![allow(unused)]
fn main() {
extern "system" fn callback(
    _: *mut TP_CALLBACK_INSTANCE,
    _: *mut std::ffi::c_void,
    _: *mut TP_WORK,
) {
    let mut counter = COUNTER.write().unwrap();
    *counter += 1;
}
}

The parameters can safely be ignored but do come in handy from time to time. At this point, we have a valid work object but nothing is happening yet. In order to kick off some "work", we need to submit the work object to the thread pool. You can do so as many times as you'd like, so lets go ahead and do it ten times:

#![allow(unused)]
fn main() {
for _ in 0..10 {
    SubmitThreadpoolWork(work);
}
}

You can now expect the callbacks to run concurrently, hence the RwLock above. Of course, with all of that concurrency we need some way to tell when the work is done. That's the job of the WaitForThreadpoolWorkCallbacks function:

#![allow(unused)]
fn main() {
WaitForThreadpoolWorkCallbacks(work, false);
}

The second parameter indicates whether we would like to cancel any pending callbacks that have not started to execute. Passing false here thus indicates that we would like the wait function to block until all of the submitted work has completed. At that point, we can safely close the work object to free its memory:

#![allow(unused)]
fn main() {
CloseThreadpoolWork(work);
}

And just to prove that it works reliably, we can print out the counter's value:

#![allow(unused)]
fn main() {
let counter = COUNTER.read().unwrap();
println!("counter: {}", *counter);
}

Running the sample should print something like this:

counter: 10

Here's the full sample for reference.

Calling your first API with the windows-sys crate

So you want to get a feel for calling a simple Windows API. Where to start? Let's look at a relatively simple API for submitting callbacks to the thread pool. You can read more about this API here.

The first step is to add a dependency on the windows-sys crate and indicate which features you'd like to access:

[dependencies.windows-sys]
version = "0.48"
features = [
    "Win32_Foundation",
    "Win32_System_Threading",
]

Why these two features? Well, the thread pool API is defined in the Win32::System::Threading module and we'll also use a handful of definitions from the Win32::Foundation module. If you're unsure, the docs for any given API provide a helpful comment indicating which features are required. For example, here are the docs for WaitForThreadpoolWorkCallbacks where you can see it depends on both of these features since it is defined in the Win32::System::Threading module and depends on BOOL which is defined in the Win32::Foundation module.

Cargo will now handle the heavy lifting, tracking down the dependencies and making sure the import libs are present, so that we can simply call these APIs in Rust without any further configuration. We can employ a use declaration to make these APIs a little more accessible:

#![allow(unused)]
fn main() {
use windows_sys::{Win32::Foundation::*, Win32::System::Threading::*};
}

In order to "prove" that the code works and yet keep it real simple let's just use the thread pool to increment a counter some number of times. Here we can use a reader-writer lock for safe and multi-threaded access to the counter variable:

#![allow(unused)]
fn main() {
static COUNTER: std::sync::RwLock<i32> = std::sync::RwLock::new(0);
}

For this example, I'll just use a simple main function with a big unsafe block since virtually everything here is going to be unsafe. Why is that? Well the windows crate lets you call foreign functions and these are generally assumed to be unsafe.

fn main() {
    unsafe {
        
    }
}

The thread pool API is modeled as a set of "objects" exposed via a traditional C-style API. The first thing we need to do is create a work object:

#![allow(unused)]
fn main() {
let work = CreateThreadpoolWork(Some(callback), std::ptr::null_mut(), std::ptr::null());
}

The first parameter is a pointer to a callback function. The remaining parameters are optional and you can read more about them in my thread pool series on MSDN.

Since this function allocates memory, it is possible that it might fail, and this is indicated by returning a null pointer rather than a valid work object handle. We'll check for this condition and call the GetLastError function to display any relevant error code:

#![allow(unused)]
fn main() {
if work.is_null() {
    println!("{:?}", GetLastError());
    return;
}
}

The callback itself must be a valid C-style callback according to the signature expected by the thread pool API. Here's a simple callback that will increment the count:

#![allow(unused)]
fn main() {
extern "system" fn callback(
    _: *mut TP_CALLBACK_INSTANCE,
    _: *mut std::ffi::c_void,
    _: *mut TP_WORK,
) {
    let mut counter = COUNTER.write().unwrap();
    *counter += 1;
}
}

The parameters can safely be ignored but do come in handy from time to time. At this point, we have a valid work object but nothing is happening yet. In order to kick off some "work", we need to submit the work object to the thread pool. You can do so as many times as you'd like, so lets go ahead and do it ten times:

#![allow(unused)]
fn main() {
for _ in 0..10 {
    SubmitThreadpoolWork(work);
}
}

You can now expect the callbacks to run concurrently, hence the RwLock above. Of course, with all of that concurrency we need some way to tell when the work is done. That's the job of the WaitForThreadpoolWorkCallbacks function:

#![allow(unused)]
fn main() {
WaitForThreadpoolWorkCallbacks(work, 0);
}

The second parameter indicates whether we would like to cancel any pending callbacks that have not started to execute. Passing 0, meaning false, here thus indicates that we would like the wait function to block until all of the submitted work has completed. At that point, we can safely close the work object to free its memory:

#![allow(unused)]
fn main() {
CloseThreadpoolWork(work);
}

And just to prove that it works reliably, we can print out the counter's value:

#![allow(unused)]
fn main() {
let counter = COUNTER.read().unwrap();
println!("counter: {}", *counter);
}

Running the sample should print something like this:

counter: 10

Here's the full sample for reference.

Calling your first COM API

COM APIs are unique in that they expose functionality through interfaces. An interface is just a collection of virtual function pointers grouped together in what is known as a vtable, or virtual function table. This is not something that Rust supports directly, like C++ does, but the windows crate provides the necessary code gen to make it possible and seamless. A COM API will still typically start life through a traditional C-style function call in order to get your hands on a COM interface. From there you might call other methods via the interface.

Some COM-based APIs can get real complicated so let’s start with a very simple example. The CreateUri function is officially documented on MSDN as returning the IUri interface representing the results of parsing the given URI. The Rust docs for the windows crate indicate that it resides in the Win32::System::Com module so we can configure our windows crate dependency accordingly:

[dependencies.windows]
version = "0.48"
features = [
    "Win32_System_Com",
]

And we can employ a use declaration to make this API a little more accessible. The windows crate's core module also provides a few helpers to make it easier to work with COM interfaces, so we’ll include that as well:

#![allow(unused)]
fn main() {
use windows::{core::*, Win32::System::Com::*};
}

For this example, I'll just use a simple main function with a big unsafe block since virtually everything here is going to be unsafe. Why is that? Well the windows crate lets you call foreign functions and these are generally assumed to be unsafe.

fn main() -> Result<()> {
    unsafe {
        
        Ok(())
    }
}

The only "interesting" point here is the use of the Result type from the windows::core module that provides Windows error handling to simplify the following API calls. And with that, we can call the CreateUri function as follows:

#![allow(unused)]
fn main() {
let uri = CreateUri(w!("http://kennykerr.ca"), Uri_CREATE_CANONICALIZE, 0)?;
}

There's quite a lot going on here. The first parameter is actually a PCWSTR, representing a null-terminated wide string used by many Windows APIs. The windows crate provides the handy w! macro for creating a valid null-terminated wide string as a compile-time constant. The second parameter is just the default flag specified by the official documentation. The third parameter is reserved and should thus be zero.

The resulting IUri object has various methods that we can now use to inspect the URI. The official documentation describes the various interface methods and the Rust docs give you a quick glimpse at their various signatures so that you can quickly figure out how to call them in Rust. For this example, let’s just call two of them to print out the URI's domain and the HTTP port number:

#![allow(unused)]
fn main() {
let domain = uri.GetDomain()?;
let port = uri.GetPort()?;

println!("{domain} ({port})");
}

Under the hood, those methods will invoke the virtual functions through the COM interface and into the implementation provided by the API. They also provide a bunch of error and signature transformation to make it very natural to use from Rust. And that’s it, running the sample should print something like this:

kennykerr.ca (80)

Here's the full sample for reference.

Calling your first WinRT API

Windows 8 introduced the Windows Runtime, which at its heart, is just COM with a few more conventions thrown in to make language bindings appear more seamless. The windows crate already makes calling COM APIs far more seamless than it is for C++ developers, but WinRT goes further by providing first-class support for modeling things like constructors, events, and class hierarchies. In calling your first COM API, we saw that you still had to bootsrap the API with a C-style DLL export before calling COM interface methods. WinRT works the same way but abstracts this away in a generalized manner.

Let’s use a simple example to illustrate. The XmlDocument "class" models an XML document that can be loaded from various sources. The Rust docs for the windows crate indicate that this type resides in the Data::Xml::Dom module so we can configure our windows crate dependency as follows:

[dependencies.windows]
version = "0.48" 
features = [
    "Data_Xml_Dom",
]

And we can employ a use declaration to make this API a little more accessible. The windows crate's core module just provides a few helpers to make it easier to work with Windows APIs, so we'll include that as well:

#![allow(unused)]
fn main() {
use windows::{core::*, Data::Xml::Dom::XmlDocument}; 
}

For this example, I'll just use a simple main function with a Result type from the windows::core module to provide automatic error propagation and simplify the subsequent API calls:

fn main() -> Result<()> {

    Ok(())
}

You'll notice that unlike the previous Win32 and COM examples, this main function does not need an unsafe block since WinRT calls are assumed to be safe thanks to its more constrained type-system.

To begin, we can simply call the new method to create a new XmlDocument object:

#![allow(unused)]
fn main() {
let doc = XmlDocument::new()?;
}

This looks a lot more like an idiomatic Rust type than your typical COM API, but under the hood a similar mechanism is used to instantiate the XmlDocument implementation via a DLL export. We can then call the LoadXml method to test it out. There are various other options for loading XML from different sources, which you can read about in the official documentation or from the Rust docs for the XmlDocument API. The windows crate also provides the handy h! macro for creating an HSTRING, the string type used by WinRT APIs:

#![allow(unused)]
fn main() {
doc.LoadXml(h!("<html>hello world</html>"))?;
}

And just like that, we have a fully-formed Xml document that we can inspect. For this example, let's just grab the document element and then do some basic queries as follows:

#![allow(unused)]
fn main() {
let root = doc.DocumentElement()?;
assert!(root.NodeName()? == "html");
println!("{}", root.InnerText()?);
}

First we assert that the element's name is in fact "html" and then print out the element's inner text. As with the previous COM example, those methods all invoke virtual functions through COM interfaces, but the windows crate makes it very simple to make such calls directly from Rust. And that's it, running the sample should print something like this:

hello world

Here's the full sample for reference.

How do I query for a specific COM interface?

COM and WinRT interfaces in the windows crate implement the Interface trait. This trait provides the cast method that will use QueryInterface under the hood to cast the current interface to another interface supported by the object. The cast method returns a Result<T> so that failure can be handled in a natural way in Rust.

For example, it is often necesary to get the IDXGIDevice interface for a given Direct3D device to interop with other rendering APIs. This is how you might create a swap chain for drawing and presenting to a Direct3D device. Let's imagine a simple function that accepts a Direct3D device and returns the underlying DXGI factory:

#![allow(unused)]
fn main() {
fn get_dxgi_factory(device: &ID3D11Device) -> Result<IDXGIFactory2> {
}
}

The first thing you need to do is query or cast the Direct3D device for its DXGI interface as follows:

#![allow(unused)]
fn main() {
let device = device.cast::<IDXGIDevice>()?;
}

If its more convenient, you can also make use of type inference as follows:

#![allow(unused)]
fn main() {
let device: IDXGIDevice = device.cast()?;
}

With the COM interface in hand, we need an unsafe block to call its methods:

#![allow(unused)]
fn main() {
unsafe {
}
}

Within the unsafe block, we can retrieve the device's physical adapter:

#![allow(unused)]
fn main() {
let adapter = device.GetAdapter()?;
}

And just for fun (or debugging), we might print out the adapter's name:

#![allow(unused)]
fn main() {
if cfg!(debug_assertions) {
    let mut desc = Default::default();
    adapter.GetDesc(&mut desc)?;
    println!("{}", String::from_utf16_lossy(&desc.Description));
}
}

Finally, we can return the adapter's parent and also the DXGI factory object for the device:

#![allow(unused)]
fn main() {
adapter.GetParent()
}

Running the sample I get the following impressive results:

AMD FirePro W4100

Here's a more comprehensive DirectX example.

The cast method works equally well for WinRT classes and interfaces. It is particularly useful for interop with WinRT APIs.

Understanding the windows-targets crate

The windows and windows-sys crates depend on the windows-targets crate for linker support. The windows-targets crate includes import libs, supports semantic versioning, and optional support for raw-dylib. It provides explicit import libraries for the following targets:

  • i686_msvc
  • x86_64_msvc
  • aarch64_msvc
  • i686_gnu
  • x86_64_gnu
  • x86_64_gnullvm
  • aarch64_gnullvm

An import lib contains information the linker uses to resolve external references to functions exported by DLLs. This allows the operating system to identify a specific DLL and function export at load time. Import libs are both toolchain- and architecture-specific. In other words, different lib files are required depending on whether you're compiling with the MSVC or GNU toolchains and whether you're compiling for the x86 or ARM64 architectures. Note that import libraries don't contain any code, as static libraries do.

While the GNU and MSVC toolchains often provide some import libs to support C++ development, those lib files are often incomplete, missing, or just plain wrong. This can lead to linker errors that are very difficult to diagnose. The windows-targets crate ensures that all functions defined by the windows and windows-sys crates can be linked without relying on implicit lib files distributed by the toolchain. This ensures that dependencies can be managed with Cargo and streamlines cross-compilation. The windows-targets crate also contains version-specific lib file names ensuring semver compatibility. Without this capability, the linker will simply pick the first matching lib file name and fail to resolve any missing or mismatched imports.

Note: Ordinarily, you don't need to think about the windows-targets crate at all. The windows and windows-sys crates depend on the windows-targets crate automatically. Only in rare cases will you need to use it directly.

Start by adding the following to your Cargo.toml file:

[dependencies.windows-targets]
version = "0.48"

Use the link macro to define the external functions you wish to call:

#![allow(unused)]
fn main() {
windows_targets::link!("kernel32.dll" "system" fn SetLastError(code: u32) -> ());
windows_targets::link!("kernel32.dll" "system" fn GetLastError() -> u32);
}

Make use of any Windows APIs as needed:

fn main() {
    unsafe {
        SetLastError(1234);
        assert_eq!(GetLastError(), 1234);
    }
}

By default the link macro will cause the linker to use the bundled import libs. Compiling with the windows_raw_dylib Rust build flag will cause Cargo to skip downloading the import libs altogether and instead use raw-dylib to resolve imports automatically. The Rust compiler will then create the import entries directly. This works without having to change any of your code. Without the windows-targets crate, switching between linker and raw-dylib imports requires very intricate code changes. As of this writing, the raw-dylib feature is not yet stable.

Standalone code generation

Even with a choice between the windows and windows-sys crates, some developers may prefer to use completely standalone bindings. The windows-bindgen crate lets you generate entirely standalone bindings for Windows APIs with a single function call that you can run from a test to automate the generation of bindings. This can help to reduce your dependencies while continuing to provide a sustainable path forward for any future API requirements you might have, or just to refresh your bindings from time to time to pick up any bug fixes automatically from Microsoft.

Warning: Standalone code generation should only be used as a last resort for the most demanding scenarios. It is much simpler to use the windows-sys crate and let Cargo manage this dependency. This windows-sys crate provides raw bindings, is heavily tested and widely used, and should not meaningfully impact your build time.

Start by adding the following to your Cargo.toml file:

[dependencies.windows-targets]
version = "0.48"

[dev-dependencies.windows-bindgen]
version = "0.48"

The windows-bindgen crate is only needed for generating bindings and is thus a dev dependency only. The windows-targets crate is a dependency shared by the windows and windows-sys crates and only contains import libs for supported targets. This will ensure that you can link against any Windows API functions you may need.

Write a test to generate bindings as follows:

#![allow(unused)]
fn main() {
#[test]
fn gen_bindings() {
    let apis = [
        "Windows.Win32.System.SystemInformation.GetTickCount",
    ];

    let bindings = windows_bindgen::standalone(&apis);
    std::fs::write("src/bindings.rs", bindings).unwrap();
}
}

Make use of any Windows APIs as needed.

mod bindings;
use bindings::*;

fn main() {
    unsafe {
        println!("{}", GetTickCount());
    }
}