Author Archives: Kenny Kerr

Choosing between the windows and windows-sys crates

The windows crate provides bindings for the Windows API, including C-style APIs as well as COM and WinRT APIs. 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.

Getting started with windows-rs

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.

Rust and Direct2D

I have been hard at work improving the API coverage of Rust for Windows and reached a little milestone with the windows crate being advanced enough to build a complete desktop app with hardware-accelerated rendering and animation using Direct2D. This is actually a Rust port of a C++ sample I wrote for one of my Pluralsight courses. Enjoy!

https://github.com/kennykerr/samples-rs

Rust for Windows

I am excited to finally talk about the grand plan we have been working on for some time, namely the unification of the Windows API. No more Win32 here, WinRT there, COM this, UWP that. Just stop it. Rust for Windows lets you use any Windows API directly and seamlessly via the windows crate.

Whether its timeless functions like CreateEvent and WaitForSingleObject, powerful graphics engines like Direct3D, traditional windowing functions like CreateWindowEx and DispatchMessage, or more recent UI frameworks like Composition and Xaml, the windows crate has you covered.

This is an early preview, but finally having metadata for the entire Windows API is a huge step toward making Windows development easier and more approachable for all developers.

The repo has everything you need to get started:

https://github.com/microsoft/windows-rs/

In particular, the readme has a short guide to getting started. There are also some simple examples that you can follow. And of course, we have updated Robert Mikhayelyan’s Minesweeper port.

If having the entire Windows API at your fingertips seems a little daunting, I have also published some Rust documentation for the Windows API. This lets you browse or search for just the API you need and makes it a lot easier to find what you are looking for.

If you have questions or run into issues, please use the GitHub repo to get in touch.

Repo: https://github.com/microsoft/windows-rs

API docs: https://microsoft.github.io/windows-docs-rs

Samples: https://github.com/kennykerr/samples-rs

https://blogs.windows.com/windowsdeveloper/2021/01/20/making-win32-apis-more-accessible-to-more-languages

Improving the IDE for Rust/WinRT

We’ve looked at the basics of getting started with Rust/WinRT and how to optimize your inner loop by reducing the build time thanks to Cargo’s caching of dependencies. Now let’s look at improving the quality of the development experience inside VS Code.

The main problem with Rust/WinRT’s import macros is that it doesn’t generate Rust code. Instead, it generates a token stream that is directly ingested by the Rust compiler. While this is quite efficient, it can be less than desirable from a developer’s perspective. Without Rust code, it becomes very difficult to debug into the generated code. Rust code is also required by the rust-analyzer VS Code extension in order to provide code completion hints.

Fortunately, Rust/WinRT provides a build macro to complement the import macro we’ve already been using. Both macros accept the same input syntax for describing dependencies and types to be imported. The difference is that the build macro helps to generate actual Rust code that you can read, include, and step through in a debugger.

The first thing we’ll do is add a build script to the bindings sub crate we created in the last installment. Note that it is intentionally called a build script and not a source file. Even though it’s just Rust code, its compiled separately. The way cargo knows it’s the build script is to call the file build.rs and place it in the root of the package, not inside the src folder. Now add the following to the build.rs file you created in the bindings package:

winrt::build!(
    dependencies
        os
    types
        windows::system::diagnostics::*
);

fn main() {
    build();
}

Again, the tokens within the build macro are exactly the same as those we previously used with the import macro. Only the name of the macro itself has changed. Due to certain limitations in the Rust compiler, we need to generate the code inside the main function but cannot actually place the build macro inside the main function. So we have this awkward dance where the build macro really just generates a build function that the main function then calls to generate the Rust code for the package. Once the issues with the Rust compiler have been resolved, we should be able to streamline this process.

To underscore that the build script is compiled separately, we need to add winrt as a distinct dependency of the build inside the bindings project’s Cargo.toml file:

[dependencies]
winrt = "0.7.0"

[build-dependencies]
winrt = "0.7.0"

This ensures that the dependency is available to the build script without having to be available to the project’s source code. You can of course have the same dependency in both if that were needed, as it is in this case.

Now inside the binding project’s src/lib.rc file we can include the Rust code generated in the build script, instead of calling the import macro:

include!(concat!(env!("OUT_DIR"), "/winrt.rs"));

Note that the OUT_DIR environment variable is only available if the project has a build script. It’s also why the build macro couldn’t just generate the winrt.rs file directly: the OUT_DIR environment variable is only set when the build script is executed and not when it is compiled, which is when the build macro is executed.

And that’s all we need to do to switch from using the import macro to the build macro. You can now recompile the sample project and you should find it works just as it did before. The difference is that now we can both debug the code and make use of code completion hints.

Debugging can be achieved either with the Microsoft C/C++ VS Code extension or with the CodeLLDB extension in combination with the rust-analyzer extension. Once you’ve picked an appropriate extension, you can simply begin debugging and step into any of the generated code and you’ll land up somewhere inside the generated winrt.rs source file. The build macro even ensures that the Rust code is properly formatted for readability.

Code completion also works reasonably well with the rust-analyzer extension, but it does have a few limitations and can struggle a bit with the sheer amount of code that Rust/WinRT generates. I’ll give you two tips to help you get started.

The first is to ensure that rust-analyzer can find the generated code. That’s what the “Load Out Dirs From Check” setting is for. Make sure this is checked:

The second is to place any use declarations at the top of your Rust source file, otherwise rust-analyzer will fail to correctly produce code completion hints. Last time, we wrote the use declaration inside the main function. That won’t do. Instead, update the main.rs source file as follows:

use bindings::windows::system::diagnostics::*;

fn main() -> winrt::Result<()> {
    for process in ProcessDiagnosticInfo::get_for_processes()?
        .into_iter()
        .take(5)
    {
        println!(
            "id: {:5} packaged: {:5} name: {}",
            process.process_id()?,
            process.is_packaged()?,
            process.executable_file_name()?
        );
    }
 
    Ok(())
}

Now, you should be able to rely on rust-analyzer to provide code completion hints:

If code completion isn’t working too well or you just want to browse available APIs you still have options. You can go to the official documentation for Windows APIs. Of course, you’ll need to translate the C#/C++ specific naming conventions to Rust. Alternatively, you can get Cargo to generate documentation for the generated bindings:

C:\sample\bindings>cargo doc --open
    Updating crates.io index
  Downloaded quote v1.0.7
  Downloaded serde_json v1.0.54
   Compiling proc-macro2 v1.0.18
.
.
.
    Finished dev [unoptimized + debuginfo] target(s) in 32.09s
     Opening C:\sample\bindings\target\doc\bindings\index.html

Cargo will open the browser where you search or browse for any of the available APIs. Naturally, this will only include APIs that were generated by the build macro. If you’d like to see more, simply add more types to the build macro and rerun Cargo.

Optimizing the build with Rust/WinRT

In Getting started with Rust/WinRT we used the import macro to generate Rust bindings for Windows APIs directly into the Rust module where the import macro is used. This can be a nested module if you wish. Here’s an example using the Windows.System.Diagnostics namespace, which is documented here.

mod bindings {
    winrt::import!(
        dependencies
            os
        types
            windows::system::diagnostics::*
    );
}

Notice how in the following main function, I now use bindings as part of the Rust path for the windows::system::diagnostics module:

fn main() -> winrt::Result<()> {
    use bindings::windows::system::diagnostics::*;

    for process in ProcessDiagnosticInfo::get_for_processes()? {
        println!(
            "id: {:5} packaged: {:5} name: {}",
            process.process_id()?,
            process.is_packaged()?,
            process.executable_file_name()?
        );
    }

    Ok(())
}

This will give you a quick dump of the processes currently running on your machine:

C:\sample>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 10.54s
     Running `target\debug\sample.exe`
id:     4 packaged: false name: System
id:   176 packaged: false name: Secure System
id:   284 packaged: false name: Registry
id:  8616 packaged: true  name: RuntimeBroker.exe
id: 10732 packaged: false name: svchost.exe
.
.
.

As I pointed out last time, the time it takes can quickly become prohibitive. One option is to implement the bindings module in a separate bindings.rs file. While this will give a marginal improvement, Cargo is far better at caching the results if you stick the code in its own crate. Back in the console, let’s add a sub crate to house the generated bindings.

C:\sample>cargo new --lib bindings
     Created library `bindings` package

We then need to update the outer project to tell Cargo that it now depends on this new bindings library. To do that, we need to add bindings as a dependency in the Cargo.toml file for the sample project:

[dependencies]
winrt = "0.7.0"
bindings = { path = "bindings" }

While the first dependency is resolved via crates.io the bindings dependency uses a relative path to find the sub crate. This is all it takes to ensure that cargo will automatically build and cache the new dependency. Now let’s get the bindings library configured to import the WinRT types. Inside the bindings project, open the Cargo.toml file where we can add the winrt dependency:

[dependencies]
winrt = "0.7.0"

We can then simply remove the import macro from the original project’s main.rs source file and add it to the bindings project’s lib.rs source file:

winrt::import!(
    dependencies
        os
    types
        windows::system::diagnostics::*
);

The first time I run the example, it completes in about 10 seconds:

C:\sample>cargo run
   Compiling bindings v0.1.0 (C:\sample\bindings)
   Compiling sample v0.1.0 (C:\sample)
    Finished dev [unoptimized + debuginfo] target(s) in 10.61s
     Running `target\debug\sample.exe`
id:     4 packaged: false name: System
id:   176 packaged: false name: Secure System
.
.
.

Now let’s make a small change to see whether incremental build time improves. Back in the sample project’s main function, we can turn the vector returned by get_for_processes into an iterator and limit the results to the first 5 processes:

fn main() -> winrt::Result<()> {
    use bindings::windows::system::diagnostics::*;

    for process in ProcessDiagnosticInfo::get_for_processes()?
        .into_iter()
        .take(5)
    {
        println!(
            "id: {:5} packaged: {:5} name: {}",
            process.process_id()?,
            process.is_packaged()?,
            process.executable_file_name()?
        );
    }

    Ok(())
}

Cargo does quick work of recompiling and gets us running in under a second:

C:\sample>cargo run
   Compiling sample v0.1.0 (C:\sample)
    Finished dev [unoptimized + debuginfo] target(s) in 0.69s
     Running `target\debug\sample.exe`
id:     4 packaged: false name: System
id:   176 packaged: false name: Secure System
id:   284 packaged: false name: Registry
id:  1064 packaged: false name: smss.exe
id:  1484 packaged: false name: csrss.exe

Now that’s much better. But there’s more! Using the import macro has a few drawbacks, but I’ll talk about creating your own build script next time. So stay tuned!

Getting started with Rust/WinRT

Getting started with Rust/WinRT is quite simple thanks in large part to the polished toolchain that Rust developers enjoy. Here are a few links if you don’t need any help getting started. Read on if you’d like to learn some tips and tricks to make the most out of the Windows Runtime.

GitHub: https://github.com/microsoft/winrt-rs
Docs.rs: https://docs.rs/winrt/
Crates.io: https://crates.io/crates/winrt

Install the following prerequisites:

Visual Studio 2019 – be sure to install the C++ tools as this is required by the Rust compiler (only the linker is required).
Visual Studio Code – this is the default IDE used for Rust.
Python – be sure to install the x64 version as this is required for debugging support.
Git – Rust has deep support for Git.
Rust – this installs `rustup` which is a tool for installing Rust toolchains and common Rust related tooling.

Now open VS Code and type `Ctrl+Shift+X` to open the extensions panel and install the following extensions:

rust-analyzer – there are others, but this is the only Rust extension that I’ve tried that actually works reliably most of the time.
CodeLLDB – you can also use the Microsoft C++ extension for debugging, but this one does a better job of integrating with the IDE.
C/C++ – the Microsoft C++ extension doesn’t integrate as well with the IDE, but provides superior debugging information, so you may want to have that on hand for an emergency.

You should be prompted to download and install the Rust language server. Go ahead and let that install. You may need to restart VS Code and give it a few moments to load, after which it should all be ready and working pretty well.

Let’s now start real simple with a new cargo package:

C:\>cargo new sample
     Created binary (application) `sample` package

Cargo is Rust’s package manager. This command will create a minimal project that you can open with VS Code:

C:\>cd sample
C:\sample>code .

Open the Cargo.toml file that cargo created for the project and add the WinRT crate as a dependency:

[dependencies]
winrt = "0.7.0"

That’s the current version as I write this, but you can check Crates.io for the latest version. You can use cargo once again to build the application:

C:\sample>cargo build
    Updating crates.io index
   Compiling proc-macro2 v1.0.18
   Compiling unicode-xid v0.2.0
   Compiling syn v1.0.30
   Compiling ryu v1.0.5
   Compiling serde v1.0.111
   Compiling itoa v0.4.5
   Compiling sha1 v0.6.0
   Compiling quote v1.0.6
   Compiling serde_json v1.0.53
   Compiling winrt_gen_macros v0.7.0
   Compiling winrt_gen v0.7.0
   Compiling winrt_macros v0.7.0
   Compiling winrt v0.7.0
   Compiling sample v0.1.0 (C:\sample)
    Finished dev [unoptimized + debuginfo] target(s) in 19.65s

The first time you run cargo, it goes ahead and downloads any dependencies recursively. This might seem like a lot, but it is not unusual for Rust crates to depend on a variety of other crates. The good news is that cargo will cache the compiled crates and reuse those results, ensuring subsequent builds are very snappy. You can also have cargo run the application directly, which will rebuild the application if necessary:

C:\sample>cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target\debug\sample.exe`
Hello, world!

Notice how the initial build took 20 seconds, while the subsequent run hardly took any time at all. Of course, nothing had changes so let’s change that. In the project’s src folder you’ll find a main.rs source file. There you can see the source of the Hello world greeting. Let’s now use the winrt::import macro to generate Rust bindings for WinRT APIs.

winrt::import!(
    dependencies
        os
    types
        windows::data::xml::dom::*
);

It doesn’t really matter where in main.rs you put this code, but I usually put it at the top as it logically includes a bunch of Rust code that you can then use in your application. The import macro has two parts. There are the dependencies that identify the WinRT components you wish to make use of in your application and the specific subset of types within those dependencies that you actually want to use. In this case, I’ve used “os” to make use of all available operating system APIs. Those correspond to the version of Windows that you happen to be running on. It’s also possible to target a specific Windows SDK version. I’ve also constrained the import macro to just those types in the windows::data::xml::dom module. This corresponds to the Windows.Data.Xml.Dom namespace in the official documentation. As you might have guessed, this is a Rust path and you can certainly constrain it further to include only specific types within different modules if you wish.

Let’s now replace the main function provided by cargo with something a little more interesting. Here I’m using the XmlDocument struct generated by the import macro, which is documented here.

fn main() -> winrt::Result<()> {
    use windows::data::xml::dom::*;

    let doc = XmlDocument::new()?;
    doc.load_xml("<html>hello world</html>")?;

    let root = doc.document_element()?;
    assert!(root.node_name()? == "html");
    assert!(root.inner_text()? == "hello world");

    Ok(())
}

If you were to recompile at this point, you may notice it taking just a little while:

C:\sample>cargo run
   Compiling sample v0.1.0 (C:\sample)
    Finished dev [unoptimized + debuginfo] target(s) in 8.71s
     Running `target\debug\sample.exe`

8 seconds isn’t so bad, but as you add more types the import macro will naturally have more work to do and the Rust compiler will spend more time processing the results. The time it takes can quickly become prohibitive and a more scalable solution is needed. Still, the import macro is handy for getting started or just quickly calling a specific Windows API. Another option is to use a Rust build script to generate and cache the results of importing WinRT types.

We’ll cover that next time, so stay tuned!

Rust/WinRT is now on GitHub

We are excited to announce that the Rust/WinRT project finally has a permanent and public home on GitHub:

https://github.com/microsoft/winrt-rs

A lot has happened around the world since my last update. I hope this finds you and your family well. We have also had to adjust the way we work at Microsoft. Ryan Levick and I have been hard at work getting things ready for us to continue the development of Rust/WinRT out in the open. We look forward to hearing from all of you whether in the form of feedback or contributions. There is much to be done, but we are excited to be able to share what we have accomplished thus far.

For more information, please see the official announcement and don’t forget to try Robert Mikhayelyan’s very cool demo.