Skip to content

Latest commit

 

History

History
224 lines (162 loc) · 11 KB

getting-started.md

File metadata and controls

224 lines (162 loc) · 11 KB

Getting Started

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.

Bootstrapping the project

To get started, create a new Rust project named "spellchecker":

cargo new spellchecker

We'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 --lib

We 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:

image

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::Suggest method takes an argument of type *mut Option<IEnumString>. If IEnumString (which is located in the Windows::Win32::System::Com namespace), is not generated, ISpellChecker::Suggest will 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 crate

And 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.

Initialization Code

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(())
}

Using the generated APIs

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 ISpellCheckerFactor::IsSupported:

pub unsafe fn IsSupported<'a>(
    &self,
    languageTag: impl IntoParam<'a, PWSTR>,
    value: *mut BOOL,
) -> HRESULT

This 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 misspelled

We 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.

Next Steps

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.