Generating metadata for use with the windows crate for Rust

Published · Last revised

The 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.)

microsoft-dia
|-- .gitignore
|-- .metadata/
|-- .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\microsoft-dia\.metadata\obj\generated\common\Microsoft.Dia.cs...
Scraping constants and enums...
Writing winmd...
Winmd emitted at: C:\Sources\microsoft-dia\.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 microsoft-dia. Let's also add a build dependency on windows-bindgen. This crate has tooling to automatically generate Rust bindings from Windows metadata.

[package]
name = "microsoft-dia"
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.53"

[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 feature on both and Win32_Foundation on the former:

[dependencies.windows]
version = "0.53"
features = [
"implement",
"Win32_Foundation",
]

[dependencies.windows-core]
version = "0.53"
features = [
"implement"
]

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 microsoft-dia::bindings::IDiaAddressMap you can now type use microsoft-dia::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.

Using the above knowledge, resolve the build errors by addding the missing windows features.

[dependencies.windows]
version = "0.44.0"
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

Revision history

January 18, 2023

  • Initial publish

January 3, 2024

  • Minor changes

February 24, 2024

  • Major changes to Rust section to account for new and simpler bindgen tooling
  • Added SymTagEnum workaround to Metadata section
  • Other minor changes and corrections