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.

3 thoughts on “Improving the IDE for Rust/WinRT

  1. Aleksander Heintz

    If you take this a step further, you could set up a repo that produces (automatically) crates for all of these bindings, so that you could just add something like `winrt-windows-system-diagnostics` as a dependency and be done with it. I think that would probably be a worthwhile effort. Also; I’ve not looked into how you generate these bindings, but if you were able to add *some* of the docs to them as well, that would make docs.rs a great place to browse what’s available in as well 🙂

    Reply
    1. Kenny Kerr Post author

      Thanks for the feedback! The trouble is that different WinRT namespaces are often interdependent, so its not easy to generate one without including its dependencies. There are also a growing number of APIs that are moving from the OS into external packages that make it more practical to generate the code on demand based on the aggregate of dependencies you actually need for a given project. But this is still something we’re wrestling through. As for docs, yes I’ve been looking into how we might add something to the generated code to provide some help.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s