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.
Kenny's courses on Pluralsight
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? | windows | windows-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 version | 1.56 | 1.56 |
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 files 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.52"
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::{core::Result, 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() -> Result<()> { unsafe { } Ok(()) }
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.
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(_: PTP_CALLBACK_INSTANCE, _: *mut std::ffi::c_void, _: PTP_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.52"
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 == 0 { 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(_: PTP_CALLBACK_INSTANCE, _: *mut std::ffi::c_void, _: PTP_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.52"
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 bootstrap 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.52"
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(()) }
Unlike the previous Win32 and COM examples, you'll notice that 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 ComInterface 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.
How do I implement an existing COM interface?
In some cases, you may need to implement an existing COM interface rather than simply calling an existing implementation provided by the operating system. This is where the implement
feature and macro come in handy. The windows crate provides optional implementation support hidden behind the implement
feature. Once enabled, the implement macro may be used to implement any number of COM interfaces. The macro takes care of implementing IUnknown
itself.
Let's implement a simple interface defined by Windows to illustrate. The IPersist
interface is defined in the Win32::System::Com
module, so we'll start by adding a dependency on the windows
crate and include the Win32_System_Com
feature:
[dependencies.windows]
version = "0.52"
features = [
"implement",
"Win32_System_Com",
]
The implement
feature unlocks the implementation support.
The implement
macro is included by the windows::core
module so we'll keep things simple by including it all as follows:
#![allow(unused)] fn main() { use windows::{core::*, Win32::System::Com::*}; }
Now its time for the implementation:
#![allow(unused)] fn main() { #[implement(IPersist)] struct Persist(GUID); }
The implement
macro will provide the necessary implementation for the IUnknown
interface's lifetime management and interface discovery for whatever interfaces are included in the attribute. In this case, only IPersist
is to be implemented.
The implementation itself is defined by a trait that follows the <interface name>_Impl
pattern and its up to us to implement it for our implementation as follows:
#![allow(unused)] fn main() { impl IPersist_Impl for Persist { fn GetClassID(&self) -> Result<GUID> { Ok(self.0) } } }
The IPersist interface, originally documented here, has a single method that returns a GUID
, so we'll just implement it by returning the value contained within our implementation. The window
crate and implement
macro will take care of the rest by providing the actual COM virtual function call and virtual function table layout needed to turn this into a heap-allocated and reference-counted COM object.
All that remains is to move, or box, the implementation into the COM implementation provided by the implement
macro through the Into
trait:
#![allow(unused)] fn main() { let guid = GUID::new()?; let persist: IPersist = Persist(guid).into(); }
At this point, we can simply treat persist
as the COM object that it is:
#![allow(unused)] fn main() { let guid2 = unsafe { persist.GetClassID()? }; assert_eq!(guid, guid2); println!("{:?}", guid); }
Here's a complete example.
How do I create stock collections for WinRT collection interfaces?
Beyond implementing COM interfaces yourself, the windows crate provides stock collection implementations for common WinRT collection interfaces. Implementing WinRT collection interfaces can be quite challenging, so this should save you a lot of effort in many cases. The implement
feature is required to make use of these stock implementations.
Let's consider a few examples. The WinRT collection interfaces are all defined in the Foundation::Collections
module, so we'll start by adding a dependency on the windows
crate and include the Foundation_Collections
feature:
[dependencies.windows]
version = "0.52"
features = [
"implement",
"Foundation_Collections",
]
Creating a collection is as simple as using the TryFrom
trait on existing Vec
or BTreeMap
, depending on the kind of collection:
WinRT interface | From |
---|---|
IIterable<T> | Vec<T::Default> |
IVectorView<T> | Vec<T::Default> |
IMapView<K, V> | BTreeMap<K::Default, V::Default> |
So if you need a IIterable
implementation of i32
values you can create it as follows:
use windows::{core::*, Foundation::Collections::*}; fn main() -> Result<()> { let collection = IIterable::<i32>::try_from(vec![1, 2, 3])?; for n in collection { println!("{n}"); } Ok(()) }
The resulting collection
will implement all of the specialized IIterable<i32>
methods.
Did you notice the T::Default
in the table above? The challenge is that when the WinRT collection contains nullable types, unlike i32
, then the collection must necessarily support a backing implementation that support expressing this. The Default
associated type just replaces T
with Option<T>
for such nullable, or reference, types.
Let's consider a slightly more contrived example. Here we'll create an IMapView
with strings for keys and interfaces for values. WinRT strings are not nullable but interfaces are. WinRT strings are represented by HSTRING
in the windows
crate and for the interface we'll just use an IStringable
implementation:
#![allow(unused)] fn main() { use windows::Foundation::*; #[implement(IStringable)] struct Value(&'static str); impl IStringable_Impl for Value { fn ToString(&self) -> Result<HSTRING> { Ok(self.0.into()) } } }
We can now create a std
collection as follows:
#![allow(unused)] fn main() { use std::collections::*; let map = BTreeMap::from([ ("hello".into(), Some(Value("HELLO").into())), ("hello".into(), Some(Value("WORLD").into())), ]); }
The Rust compiler naturally infers the exact type: BTreeMap<HSTRING, Option<IStringable>>
.
Finally, we can wrap that BTreeMap
inside a WinRT collection with the TryInto
trait as follows:
#![allow(unused)] fn main() { let map: IMapView<HSTRING, IStringable> = map.try_into()?; for pair in map { println!("{} - {}", pair.Key()?, pair.Value()?.ToString()?); } }
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. Thewindows
andwindows-sys
crates depend on thewindows-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.52"
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.52"
[dev-dependencies.windows-bindgen]
version = "0.52"
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 bindgen() { let args = [ "--out", "src/bindings.rs", "--config", "flatten", "--filter", "Windows.Win32.System.SystemInformation.GetTickCount", ]; windows_bindgen::bindgen(args).unwrap(); } }
Make use of any Windows APIs as needed.
mod bindings;
fn main() {
unsafe {
println!("{}", bindings::GetTickCount());
}
}