This tutorial will show the basics of how to use the windows crate.
This is not meant as a tutorial on Windows programming, and it assumes at least passing familiarity with the Windows API including Win32 and COM. Nor is this meant as an introduction to programming in Rust for Windows developers. You should also have a basic understanding of Rust and Cargo.
The example used here is that of a simple spellchecker utility app. The app takes a string from stdin and returns back various spellchecking suggestions. You can find the complete code for this example here.
To get started, create a new Rust project named "spellchecker":
cargo new spellcheckerWe'll be generating bindings to the Windows API on the fly from metadata. This means that as we compile our program, the windows crate will read in specialized metadata which describes the entire Windows API surface and generates just the code we need to interact with the APIs we want to.
Now we have a choice to make. We could stick the generated code into our spellchecker crate directly. However, due to how Rust compiles code, doing so would mean that our bindings are recompiled every time we want to compile our crate. In order to avoid this, we can instead put our bindings into a dedicated bindings crate which will be compiled once and cached on future recompilations.
To do this, we'll simply crate a bindings crate which our spellchecker crate will take a dependency on. Inside of the spellchecker crate's directory, run:
cargo new bindings --libWe can generate our bindings inside of the bindings crate. To do this, we'll need to add the windows crate as both a regular dependency and build dependency of the bindings crate. In the Cargo.toml for the bindings crate add the following:
# ^^ The rest of the Cargo.toml remains the same
[dependencies]
windows = "0.8" # Check https://crates.io/crates/windows for the latest version
[build-dependencies]
windows = "0.8"Now that the bindings crate depends on the windows crate, we can generate our bindings inside of a build script. In case you're not familiar with build scripts, they're simply Rust files that get run automatically by cargo when building a Rust crate. To add one, we simply put a "build.rs" file in the bindings crate's directory. We'll add the windows::build! macro which is where we declare which Windows APIs we want to generate bindings for.
The question now is: which APIs do we import? Knowing exactly which API you want to use requires familiarity with the Windows API. Let's assume you've explored the documentation and are sure that you want to use the internationalization APIs and in particular the ISpellChecker (and the related ISpellCheckerFactory) located in the spellcheck functionality for this app. To know how this API is translated into Rust, go to the Rust for Windows and search for what you need.
Doing the search should lead you to find the following:
Remember the module path that ISpellChecker is in underneath bindings: Windows::Win32::Globalization. You'll want to use that exactly when specifying the types you need to generate.
Note It's important to note that for COM and WinRT APIs, methods are only available when all the types use in those methods parameters and return types are also generated. When you want to use a certain type's method, make sure you're generating everything you need to use that method. For example, the
ISpellChecker::Suggestmethod takes an argument of type*mut Option<IEnumString>. IfIEnumString(which is located in theWindows::Win32::System::Comnamespace), is not generated,ISpellChecker::Suggestwill not be generated as well.
In the build.rs file add the following:
fn main() {
windows::build! {
// Note that we're using the `Intl` namespace which is nested inside the `Win32` namespace
// which itself is inside the `Windows` namespace.
Windows::Win32::Globalization::{ISpellChecker, SpellCheckerFactory, ISpellCheckerFactory, CORRECTIVE_ACTION, IEnumSpellingError, ISpellingError},
Windows::Win32::System::SystemServices::{BOOL, PWSTR, S_FALSE},
Windows::Win32::System::Com::IEnumString
};
}We're not done! This generates the bindings, but it just puts them into the target folder of our crate where they'll go unused. We want to actually use this generated code not just generate and forget about it. In order for the generated code to be exported from the bindings crate, we need to include it in the crate itself. Change the lib.rs file of the bindings crate to the following:
windows::include_bindings!();This effectively copy/pastes the generated code into the lib.rs file. The bindings crate now exports all the bindings we've generated. Next, we need to make sure our spellchecker crate depends on the bindings crate. In the spellchecker crate's Cargo.toml file, add the bindings crate as a dependency. We'll also add the windows crate as a dependency since we'll be using that as well in our spellchecker crate.
# ^^ The rest of the Cargo.toml remains the same
[dependencies]
bindings = { path = "bindings" }
windows = "0.8" # This should match the version you used in the bindings crateAnd we're done bootstrapping the project. Now we'll move on to the code of the spellchecker crate which will use both the generated bindings from the bindings crate as well as helper functionality from the windows crate.
Inside of the main.rs file for the spellchecker crate, we'll start by adding the set up code. We'll initialize the COM runtime on the main thread. We'll make the main function return a windows::Result which is simply a standard Result type with the error hardcoded as a windows::Error.
fn main() -> windows::Result<()> {
// initialize the main thread as a multithreaded apartment
windows::initialize_mta()?;
// The rest of the code will go here!
Ok(())
}Next, we'll initialize the ISpellCheckerFactory which is what gives us access to spellcheckers. We'll first make sure the intl namespace and two types we'll need PWSTR and BOOL are in scope at the top of the main.rs file:
use bindings::Windows::Win32::Globalization;
use bindings::Windows::Win32::System::SystemServices::{BOOL, PWSTR};Then we can do the initialization by calling windows::create_instance which calls CoCreateInstance under the hood:
fn main() -> window::Result<()> {
// initialize the main thread as a multithreaded apartment
windows::initialize_mta()?;
// Create ISpellCheckerFactory
let factory: Intl::ISpellCheckerFactory = windows::create_instance(&Intl::SpellCheckerFactory)?;
// The rest of the code will go here!
Ok(())
}Next, we'll use the ISpellCheckerFactory to check that we can spell check U.S. English, and then we'll get a spellchecker instance.
Let's take a look at the method signature of ISpellCheckerFactory::IsSupported:
pub unsafe fn IsSupported<'a>(
&self,
languageTag: impl IntoParam<'a, PWSTR>,
value: *mut BOOL,
) -> HRESULTThis looks a little complicated, but it makes using the API straightforward. The method is generic on both a lifetime 'a and the trait IntoParam defined in the windows crate. Essentially, IntoParam is a slightly specialized version of Rust's std::convert::Into. It is implemented on all types that can be converted to a parameter of the type its generic over. In other words, it is any type that can be converted into a parameter of type PWSTR that lives for at least the lifetime 'a.
It turns out that IntoParam<'a, PWSTR> is implemented for &'a str so we can simply pass a string literal. IntoParam<'a, PWSTR> is also implemented on String and PWSTR itself.
fn main() -> window::Result<()> {
/// NOTE: The initialization code from above goes here!
// Make sure that the "en-US" locale is supported
let mut supported: BOOL = false.into();
let locale = "en-US";
unsafe { factory.IsSupported(locale, &mut supported).ok()? };
supported.expect("en-US is supported");
// Create a ISpellChecker
let mut checker = None;
unsafe { factory.CreateSpellChecker(locale, &mut checker).ok()? };
let checker = checker.unwrap();
Ok(())
}Now we can check our text for errors:
fn main() -> window::Result<()> {
// ... Previous code is here
// Get errors enumerator for the supplied string
let mut errors = None;
unsafe {
println!("Checking the text: '{}'", input);
checker
.ComprehensiveCheck(input.clone(), &mut errors)
.ok()?
};
let errors = errors.unwrap()
Ok(())
}Errors is of type IEnumSpellingError which allows us to access errors through a Next method. Let's loop through all the errors:
fn main() -> window::Result<()> {
// ... Previous code is here
loop {
// Get the next error in the enumerator
let mut error = None;
let result = unsafe { errors.Next(&mut error) };
// Getting S_FALSE means there are no more errors
if result == windows::HRESULT::S_FALSE {
break;
}
// We still need to check that `Next` didn't return an unexpected error
result.ok()?;
let error = error.unwrap();
}
Ok(())
}Lastly, we'll get the "corrective action" (i.e., the thing the spellchecker recommends we do to fix the spelling error) and print it to the screen:
// Inside the `loop`
// Get the corrective action
let mut action = Intl::CORRECTIVE_ACTION::CORRECTIVE_ACTION_NONE;
unsafe { error.get_CorrectiveAction(&mut action).ok()? };
println!("Corrective Action: {:?}", action);If we then try to run our code like so:
cargo run -- "Helloo world" # Note: "Hello" is misspelledWe should get the following output:
Corrective Action: CORRECTIVE_ACTION(1)
Looking at the docs for CORRECTIVE_ACTION, we can see that 1-index action corresponds to CORRECTIVE_ACTION_GET_SUGGESTIONS, meaning the spellchecking API has suggestions.
With what you've learned so far, finish the exercise of building a spellchecker that spellchecks the provided text. This will entail handling the various corrective actions and ensuring that the right thing is done for each of them.
If you get stuck, you can always see the final code here.
