A parser for RDL (Rust Definition Language) and an ECMA-335 metadata generator.
- 📦 crates.io
- 📖 docs.rs
- 🚀 Getting started
- 📁 Source
windows-rdl is the front of the metadata-authoring pipeline. It parses RDL
(Rust Definition Language) — a compact, Rust-like syntax for describing Windows
APIs — and compiles it into the same ECMA-335 .winmd metadata that
windows-bindgen consumes. It also runs the pipeline in
reverse (.winmd → canonical RDL). When an API ships only as a header and has no
metadata of its own, the companion windows-clang crate scrapes
the C/C++ headers into RDL that this crate then compiles.
Where windows-bindgen answers "I have metadata, generate Rust",
windows-rdl answers "I don't have metadata yet — produce some". The two are
designed to be used together: windows-rdl manufactures a .winmd, then
windows-bindgen turns it into bindings.rs.
Add windows-rdl as a build dependency (it is typically run from a small
codegen tool or build.rs, not shipped at runtime):
[build-dependencies]
windows-rdl = "0.3"The crate exposes three builders, one per direction of the pipeline:
reader()— RDL source →.winmdmetadata.writer()—.winmdmetadata → canonical RDL source.clang()— C/C++ headers → RDL source.
Use the reader to compile .rdl into a .winmd, and the writer to
regenerate canonical .rdl from a .winmd:
// RDL source -> winmd metadata.
windows_rdl::reader()
.input("example.rdl")
.output("example.winmd")
.write()
.unwrap();
// winmd metadata -> canonical RDL source.
windows_rdl::writer()
.input("example.winmd")
.output("example.rdl")
.write()
.unwrap();RDL can reference types it does not define (for example HRESULT or
Windows::Win32::System::Com::IUnknown). Add the standard metadata as an extra
reader input so those references resolve — the bundled metadata lives in
crates/libs/bindgen/default:
windows_rdl::reader()
.input("example.rdl")
.input("crates/libs/bindgen/default")
.output("example.winmd")
.write()
.unwrap();When an API ships only a C/C++ header, the windows-clang
crate's clang() parses it into RDL, which the reader then compiles to
metadata. Each header is parsed as its own translation unit — only its own
top-level declarations are emitted, not the things it #includes — so list every
header you need as a separate input:
windows_clang::clang()
.args(["-x", "c++", "--target=x86_64-pc-windows-msvc"])
.input("Example.h")
.input("crates/libs/bindgen/default/Windows.Win32.winmd")
.output("example.rdl")
.namespace("Example")
.library("Example.dll")
.write()
.unwrap();RDL looks like a stripped-down Rust module. A top-level mod is a metadata
namespace, tagged #[winrt] or #[win32] to choose the type system. Attributes
map to metadata attributes, and the item keywords mirror the metadata kinds.
#[win32]
mod Example {
#[repr(i32)]
enum Color {
Red = 1,
Green = 2,
Blue = 3,
}
struct Point {
x: i32,
y: i32,
}
const MAX: u32 = 42;
#[library("example.dll")]
extern fn GetPoint() -> Point;
#[guid(0x00000001_0002_0003_0004_000000000005)]
interface ICustom : Windows::Win32::System::Com::IUnknown {
fn Method(&self, value: i32) -> i32;
}
}
WinRT types use the #[winrt] namespace flavor and add runtime-class and
property syntax:
#[winrt]
mod Robotics {
#[Activatable(1)]
class Robot {
IRobot,
}
#[ExclusiveTo(Robot)]
interface IRobot {
fn Speak(&self, message: String);
Name: String;
}
}
The crates/tests/libs/rdl/input directory has a small, focused .rdl file for
each construct (struct_nested.rdl, enum_flags.rdl, delegate.rdl,
interface_generic.rdl, union.rdl, and so on) and doubles as a syntax
reference.
windows-rdl and windows-bindgen are complementary halves of one pipeline:
C/C++ headers ──clang()──► .rdl ──reader()──► .winmd ──bindgen()──► bindings.rs
(windows-rdl) (windows-rdl) (windows-bindgen)
When metadata already exists you skip straight to windows-bindgen. You reach
for windows-rdl when you need to create the metadata first — either by
hand-authoring RDL for types that have no metadata, or by lifting them out of a
C/C++ header. Two in-repo tools show both shapes:
-
tool_webviewruns the full path. WebView2 ships only a C/C++ header, soclang()producesWebView2.rdl,reader()compiles it toWebView2.winmd, andwindows_bindgen::bindgenturns that into the bindings for thewindows-webviewcrate. (crates/tools/webview/src/main.rs.) -
tool_reactorhand-authors the small set of COM interfaces and bootstrap functions that the WinUI/WinAppSDK metadata omits incrates/tools/reactor/src/extras.rdl, compiles it withreader()alongside the in-house Win32 winmd (crates/libs/bindgen/default/Windows.Win32.winmd) intoextras.winmd, and feeds that towindows_bindgen::bindgentogether with the standard metadata to generate thewindows-reactorbindings. (crates/tools/reactor/src/main.rs.) The reactor filter files (base.txt/test.txt) use the flatWindows::Win32::<Name>namespace.
In both cases the reader is given the standard metadata as an additional input
so that references from the authored RDL (Win32 handles, IUnknown, structs,
and the like) resolve against the canonical definitions.
The remainder of this page covers how the crate is built and maintained. It is
for contributors and is not needed to use windows-rdl.
The RDL grammar is parsed with syn/quote/proc-macro2, reusing Rust's own
tokenizer so the syntax stays Rust-shaped. The reader lowers that syntax tree
to ECMA-335 and emits a .winmd through
windows-metadata; the writer walks metadata read back
through the same crate to regenerate canonical RDL. The clang path uses
clang-sys to parse C/C++ translation units and project their declarations into
the RDL syntax tree, so the header and reader paths converge on the same
lowering code. A formatter module pretty-prints generated RDL.
Verified by the dedicated test crates test_rdl (RDL ↔ winmd round-trips, with
the input/*.rdl fixtures) and test_clang (header → RDL goldens under
expected/*.rdl), plus the rdl_roundtrip tool. Downstream, test_bindgen
covers the .winmd → Rust step that consumes this crate's output. Run
cargo test -p test_rdl and cargo test -p test_clang.
Beyond compiling authored RDL, windows-rdl is the reader stage of an in-repo
pipeline that builds a faithful, in-house Windows.Win32.winmd directly from
the Windows SDK headers, replacing the reference Windows.Win32.winmd from
win32metadata for the hand-authored
library crates. The windows-clang scraper (tool_win32) reads
the SDK headers, SAL annotations, and import libs and commits a browsable RDL
snapshot under metadata/win32/; this crate's reader compiles that snapshot into
the winmd. The scraper's design — the faithful-metadata principle, header
partitioning, the editorial-deviation ledger, and the canonical type remaps — is
documented in windows-clang; this section covers the winmd
artifact those tools produce.
Every maintained crate that needs Win32 metadata — the minimal-binding library
crates and windows-reactor — resolves against the in-house winmd. The only
remaining consumers of the reference winmd are the frozen monolithic windows /
windows-sys crates and the intentional parity probes.
crates/libs/bindgen/default/Windows.winmd— WinRT metadata, rebuilt in-house bytool_windowsby merging the per-contract winmds from the Windows SDK Contracts NuGet package (there is no in-house WinRT scraper — the inputs are the SDK's own reference winmds, merged rather than compiled from headers).crates/libs/bindgen/default/Windows.Win32.winmd— in-house Win32 metadata, written bytool_win32. This is whatwindows-bindgen's bundled"default"bindings resolve to.crates/libs/bindgen/default/Windows.Wdk.winmd— in-house WDK metadata, written bytool_wdk.crates/tools/package/reference/Windows.Win32.winmd/Windows.Wdk.winmd— the frozen win32metadata reference winmd, andcrates/tools/package/reference/Windows.winmd— a frozen snapshot of the in-house WinRT winmd. Together they back the publishedwindows/windows-syscrates and thetool_featurespage (and the parity probes), pinning those outputs so a live-metadata refresh never churns them.
The committed RDL snapshot (metadata/win32/*.rdl) is the reviewable source of
truth — every scrape change is a readable git diff; the merged binary winmd is
git-ignored and rebuilt by tool_win32. The build is deterministic: the writer
stages tables in BTreeMaps and the module MVID is a fixed zero GUID, so
regeneration is byte-for-byte reproducible across platforms.
tool_win32 scrapes each target architecture (x64, arm64, x86) into its own RDL
set, then merge_arch_rdl coalesces them into one winmd. A type identical across
every arch is emitted once (arch-neutral); a type that diverges is split into
per-arch copies tagged #[arch(X86|X64|Arm64)]. The collapse-or-split decision is
made structurally by the merge in windows-metadata — see
its documentation for the signature that drives it. merge_arch_rdl itself is the
orchestration: it reads each arch's RDL, runs the merge, and writes the combined
result, using a per-process scratch directory cleaned up on every return path.
tool_package and the monolithic windows / windows-sys crates stay on the
win32metadata reference winmd as frozen legacy. They do not migrate to the flat
in-house winmd: the reference winmd's 337 editorial sub-namespaces map 1:1 to 337
Cargo features, and a flat swap would collapse the published surface into one
feature-less module — a breaking change not worth making. The reference winmd is
retained at crates/tools/package/reference/ and tool_package points --in at
it explicitly. Once nothing consumes the reference winmd, windows-bindgen's
to_const_type / to_const_ptr const-wrapper reconstruction (a compatibility shim
for the reference winmd's missing const string wrappers) becomes dead and can be
removed.
- Round-trip asymmetries. A few RDL ↔ winmd forms don't round-trip byte-identically (raw identifiers, GUID constants, delegate ABI spelling). The winmd is correct either way; this is a cosmetic writer-side gap.