Generating metadata for use with the windows crate for Rust
Published · Last revisedThe windows crate for Rust has reached a sweet spot. It ships with pre-generated bindings for the entire Windows API surface and a code generator that can be used to generate your own bindings and create dependent crates. And if you pair the crate with the Windows Metadata Generator, you can transform all of your favorite C and C++ headers into Rust bindings. In fact, I already generated a few crates, such as the Microsoft Debug Interface Access (DIA) SDK, Microsoft DirectStorage SDK and the Microsoft Windows App SDK.
Let's walk through using the metadata generator to recreate most of the DIA SDK crate from scratch.
Scaffolding
First, create a new crate using a library template (cargo init --lib
).
Now, create the .windows\winmd
directory.
This directory will store metadata—Common Language Runtime (CLR) assemblies (as defined in ECMA-335) that provide a bunch of information about the Windows APIs. This information is useful to developers looking to generate bindings for different programming languages. (This path is historically where Rust for Windows stored its libraries, metadata, etc. This can be changed.)
Create a .metadata
folder as well to store the Microsoft Build (MSBuild) project and supporting files needed to generate metadata. (This can also be changed.)
dia-rs
|-- .gitignore
|--
|-- .windows/
| `-- winmd/
|-- Cargo.toml
`-- src
`-- lib.rs
Metadata project
Create .metadata\generate.proj
and populate it with some minimal XML:
<?xml version="1.0" encoding="utf-8"?>
<Project>
</Project>
This is the start of a MSBuild project.
Add the Sdk
attribute to the root node. Set its value to Microsoft.Windows.WinmdGenerator/0.59.13-preview
. (The version suffix is required as the package is not marked as stable.)
<?xml version="1.0" encoding="utf-8"?>
-<Project>
+<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
</Project>
This is the start of a Software Development Kit (SDK)-style MSBuild project.
Microsoft.Windows.WinmdGenerator
refers to a NuGet package identifier tied to the Microsoft Windows Metadata Generator SDK. This project SDK contains a bunch of actions that will execute on what we put into this project file moving forward.
Next, add a few global properties that the winmd generator needs:
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
+ <PropertyGroup Label="Globals">
+ <OutputWinmd>../.windows/winmd/Microsoft.Dia.winmd</OutputWinmd>
+ <WinmdVersion>255.255.255.255</WinmdVersion>
+ </PropertyGroup>
</Project>
-
OutputWinmd
specifies where the generator will write its output to. -
WinmdVersion
indicates the version number that will be attached to the output.Its value is not important for this crate, so it is set to its maximum value to make that clear. (If you want to share metadata externally, you may want to swap this out with a value that is consistent with Semantic Versioning.)
Now, define a partition to group together all the DIA APIs and their respective sources:
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
<PropertyGroup Label="Globals">
<OutputWinmd>../.windows/winmd/Microsoft.Dia.winmd</OutputWinmd>
<WinmdVersion>255.255.255.255</WinmdVersion>
</PropertyGroup>
+ <ItemGroup>
+ <Headers Include="..." />
+ <Partition Include="main.cpp">
+ <TraverseFiles>@(Headers)</TraverseFiles>
+ <Namespace>Microsoft.Dia</Namespace>
+ </Partition>
+ </ItemGroup>
</Project>
The partition Include
attribute points to the C/C++ file used as the generator tooling starting point. This file must be syntactically correct. That is, it must effectively compile without errors.
The partition contains several elements:
-
TraverseFiles
points to one or more headers that will be scraped for content.These headers should be reachable (directly or otherwise) from the
Include
starting point.The
Headers
property used here is not specific to the winmd generator. It is simply a MSBuild property our future include string and keep the project file tidy. -
Namespace
assigns a name to the logical grouping of functions and constants pulled out of the inputs provided.
DIA APIs are primarily described in an Interface Definition Language (IDL) file dia2.idl
and an ancillary header cvconst.h
. (The DIA SDK ships with a C/C++ header dia2.h
but it's outdated.) These files are located at %VSINSTALLDIR%\DIA SDK\idl
and %VSINSTALLDIR%\DIA SDK\include
.
Create and add additional MSBuild properties to point to those locations:
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
<PropertyGroup Label="Globals">
<OutputWinmd>../.windows/winmd/Microsoft.Dia.winmd</OutputWinmd>
<WinmdVersion>255.255.255.255</WinmdVersion>
+ <DiaSdkRoot>$(VsInstallDir)\DIA SDK</DiaSdkRoot>
+ <DiaIdlRoot>$(DiaSdkRoot)\idl</DiaIdlRoot>
+ <DiaIncRoot>$(DiaSdkRoot)\include</DiaIncRoot>
</PropertyGroup>
<ItemGroup>
<Headers Include="..." />
<Partition Include="main.cpp">
<TraverseFiles>@(Headers)</TraverseFiles>
<Namespace>Microsoft.Dia</Namespace>
</Partition>
</ItemGroup>
</Project>
Then specify the input files using these new properties.
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
<PropertyGroup Label="Globals">
<OutputWinmd>../.windows/winmd/Microsoft.Dia.winmd</OutputWinmd>
<WinmdVersion>255.255.255.255</WinmdVersion>
<DiaSdkRoot>$(VsInstallDir)\DIA SDK</DiaSdkRoot>
<DiaIdlRoot>$(DiaSdkRoot)\idl</DiaIdlRoot>
<DiaIncRoot>$(DiaSdkRoot)\include</DiaIncRoot>
</PropertyGroup>
<ItemGroup>
- <Headers Include="..." />
+ <Headers Include="$(DiaIncRoot)\cvconst.h;..." />
<Partition Include="main.cpp">
<TraverseFiles>@(Headers)</TraverseFiles>
<Namespace>Microsoft.Dia</Namespace>
</Partition>
</ItemGroup>
</Project>
But wait, you need a way to handle dia2.idl
.
To compile the IDL, add a reference to one or more IDL files with the Idl
element. The winmd generator will send these files to the IDL compiler and write out headers to the location pointed to by the built-in property CompiledHeadersDir
.
Add the DIA include root to the additional includes property so that midl can find these headers. Then add that auto-generated dia2.h
header as an input:
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.Windows.WinmdGenerator/0.59.13-preview">
<PropertyGroup Label="Globals">
<OutputWinmd>../.windows/winmd/Microsoft.Dia.winmd</OutputWinmd>
<WinmdVersion>255.255.255.255</WinmdVersion>
<DiaSdkRoot>$(VsInstallDir)\DIA SDK</DiaSdkRoot>
<DiaIdlRoot>$(DiaSdkRoot)\idl</DiaIdlRoot>
<DiaIncRoot>$(DiaSdkRoot)\include</DiaIncRoot>
+ <AdditionalIncludes>$(CompiledHeadersDir);$(DiaIncRoot)</AdditionalIncludes>
</PropertyGroup>
<ItemGroup>
+ <Idls Include="$(DiaIdlRoot)\dia2.idl" />
- <Headers Include="$(DiaIncRoot)\cvconst.h;..." />
+ <Headers Include="$(CompiledHeadersDir)\dia2.h;$(DiaIncRoot)\cvconst.h" />
<Partition Include="main.cpp">
<TraverseFiles>@(Headers)</TraverseFiles>
<Namespace>Microsoft.Dia</Namespace>
</Partition>
</ItemGroup>
</Project>
And, create the partition's main.cpp
and include the header files to be associated with the partition. Also add all dependent headers to ensure compilation succeeds:
#include <windows.h>
#include <dia2.h>
#include <cvconst.h>
We're almost done, but we need to address a bug we'll hit later. DIA (cvconst.h) has an unscoped SymTagEnum
enum declaration containing a symbolic constant with the same name. This is not possible to model in Rust and will break our crate. So add a winmd generator response file named emitter.rsp
and exclude this annoyance for now:
--exclude
SymTagEnum::SymTagEnum
Finally, in a Windows Terminal, issue a dotnet build
command. You should see something similar to the below. (You may need to launch a Visual Studio Command Prompt due to a bug at the moment.)
Determining projects to restore...
...
Compiling idl files...
Scraping headers for crossarch...
Microsoft.Dia:common - Scanning to C:\Sources\dia-rs\.metadata\obj\generated\common\Microsoft.Dia.cs...
Scraping constants and enums...
Writing winmd...
Winmd emitted at: C:\Sources\dia-rs\.windows\winmd\Microsoft.Dia.winmd
Build succeeded.
0 Warning(s)
0 Error(s)
Rust Crate: Tooling
With freshly generated metadata now available, let's work on the Rust crate.
First, edit Cargo.toml
and change the crate name to dia-rs
. Let's also add a build dependency on windows-bindgen
. This crate has tooling to automatically generate Rust bindings from Windows metadata.
[package]
name = "dia-rs"
version = "0.1.0"
edition = "2021"
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+[build-dependencies.windows-bindgen]
+version = "0.56"
[dependencies]
Time to write some code!
Create build.rs
and call into the windows_bindgen::bindgen
function with your metadata input path, output path, and instruct bindgen to filter out anything that isn't in the Microsoft.Dia namespace:
fn main() {
windows_bindgen::bindgen([
"--in",
".windows/winmd/Microsoft.Dia.winmd",
"--out",
"src/bindings.rs",
"--filter",
"Microsoft.Dia",
])
.unwrap();
}
Let's also tell Cargo to only run this script if the inputs have changed:
fn main() {
+ println!("cargo:rerun-if-changed=.windows/winmd/Microsoft.Dia.winmd");
+ println!("cargo:rerun-if-changed=build.rs");
windows_bindgen::bindgen([
"--in",
".windows/winmd/Microsoft.Dia.winmd",
"--out",
"src/bindings.rs",
"--filter",
"Microsoft.Dia",
])
.unwrap();
}
Navigate to the crate root if you're not already there and build the crate (cargo build
). After a few moments, you should now have a bunch of automatically generated code in src\bindings.rs
!
Rust Crate: DIA SDK
Let's now complete some chores to finish off the DIA crate.
First, we need to add dependencies windows
and windows-core
. The windows
crate contains our core Windows API definitions and types. windows-core
houses foundational plumbing and tooling. (More information can be found in the original pull request.)
Open Cargo.toml
and add the windows
and windows-core
crates as dependencies. Enable the implement
and Win32_Foundation
features on the former:
[dependencies.windows]
version = "0.56"
features = [
"implement",
"Win32_Foundation",
]
[dependencies.windows-core]
version = "0.56"
Then remove everything in lib.rs
, add the bindings
module, use the newly generated bindings, and finally re-export all the types within the bindings
module. This last part remove an awkward level of indirection. That is, instead of typing use dia-rs::bindings::IDiaAddressMap
you can now type use dia-rs::IDiaAddressMap
.
extern crate windows;
+pub mod bindings;
+pub use bindings::*;
Attempt to build the crate (cargo build
).
You will receive quite a few build errors. That's because we haven't brought in enough of the windows
crate to satisfy the needs of our crate. The Windows API surface is huge and to keep build times under control, the types and APIs are grouped together and tied to features in the windows
crate. To simplify development, the feature names match the type's namespace. So, for example, the ::windows::Win32::System::Com::VARIANT
type lives in the Win32::System::Com
namespace and is tied to a feature named Win32_System_Com
. Alternatively, you can also look up a type or API in the documentation or via the feature search web app.
Using the above knowledge, resolve the build errors by addding the missing windows
features.
[dependencies.windows]
version = "0.56"
features = [
"implement",
"Win32_Foundation",
+ "Win32_System_Com_StructuredStorage",
+ "Win32_System_LibraryLoader",
+ "Win32_System_Ole",
]
Attempt to build the crate again (cargo build
). Success!
Additional Tasks
At this point, you have built nearly everything in the official Microsoft DIA SDK Rust crate!
Some remaining tasks include:
- creating a helper to instantiate a DIA COM class that implements
IDiaDataSource
(hint:diaguids.lib
in the SDK has a NoRegCoCreate function)) - adding some missing constants (e.g. E_PDB_INVALID_SIG) and types (e.g. SymTagEnum)
- writing sample crates
See also
- Rust for Windows (https://github.com/microsoft/windows-rs)
- Rust for Windows crate documentation (https://microsoft.github.io/windows-docs-rs/doc/windows/)
- Rust for Windows feature search tool (https://microsoft.github.io/windows-rs/features)
- Microsoft Q&A - Windows API - Win32 (https://learn.microsoft.com/en-us/answers/tags/224/windows-api-win32)
- Stack Overflow Rust tag (https://stackoverflow.com/questions/tagged/rust)
- Win32 Metadata (https://github.com/microsoft/win32metadata)
Revision history
- Initial publish
- Minor changes
- Major changes to Rust section to account for new and simpler bindgen tooling
- Added SymTagEnum workaround to Metadata section
- Other minor changes and corrections
May 15, 2024
- Added reference to feature search web app
- Updated dependencies
- Other minor changes and corrections