The code generator that turns Windows metadata (
.winmd) into Rust bindings.
- 📦 crates.io
- 📖 docs.rs
- 🚀 Getting started
- 📁 Source
windows-bindgen generates Rust bindings from Windows metadata. It powers the
windows and windows-sys crates and is also usable directly — typically from a
build.rs — to generate a minimal, project-specific set of bindings for just the
APIs you call. The generator ships with the standard Windows metadata bundled, so
you usually only need to choose an output file and a filter.
windows-bindgen is the back half of the metadata pipeline: it expects a
.winmd and produces Rust. When an API has no metadata yet — because it ships
only as a C/C++ header, or because you are authoring types by hand — use
windows-rdl to manufacture a .winmd first, then point
windows-bindgen at it. tool_webview and tool_reactor chain the two crates
exactly this way.
Add windows-bindgen as a build dependency, and windows-link (or windows-core)
as the runtime dependency the generated code links against:
[dependencies]
windows-link = "0.2"
[build-dependencies]
windows-bindgen = "0.66"Generate bindings from build.rs. There are two equivalent entry points — a
command-line-style bindgen(args) function and a fluent Bindgen builder:
// Command-line style: the same arguments the CLI accepts.
windows_bindgen::bindgen([
"--out", "src/bindings.rs",
"--flat",
"--sys",
"--filter", "GetTickCount",
]);// Builder style: the same options as method calls.
windows_bindgen::Bindgen::new()
.output("src/bindings.rs")
.flat()
.sys()
.filter("GetTickCount")
.write();Then include! or mod the generated file and call into it:
mod bindings;
unsafe { println!("{}", bindings::GetTickCount()); }A filter selects which APIs end up in the output. Each rule may be:
- a function or type name (
GetTickCount,OSVERSIONINFOEXW), - a namespace prefix (
Windows.Win32.System.Com) that pulls in everything under it, - a fully-qualified name (
Windows.Win32.Foundation.HWND), or - a method-level entry of the form
Namespace.Type::Method— withProperty/Eventsugar for accessor pairs.
Prefix any rule with ! to exclude rather than include. Pulling in a type
automatically pulls in everything it transitively requires, so you list only the
entry points you call.
For anything beyond a handful of names, keep the arguments in a response file and
pass it with --etc. Lines starting with // are comments:
--out crates/libs/version/src/bindings.rs
--flat --sys
--filter
RtlGetVersion
OSVERSIONINFOEXW
VER_NT_WORKSTATION
windows_bindgen::bindgen(["--etc", "bindings.txt"]);This is exactly how the in-repo crates are generated — tool_bindings runs
bindgen(["--etc", "crates/tools/bindings/src/<crate>.txt"]) for each library.
Two independent choices control what the generated code looks like — its style (how rich the bindings are) and its layout (how they're organized).
Style:
- Default — full-fidelity bindings: class wrappers, inherited-interface
forwarders, ergonomic handle types, and free-function wrappers. This is what the
windowscrate ships. --sys/.sys()— raw, sys-style FFI: bareexternfunctions and plain structs, linked vialink!macros. Add--extern/.extern_fns()to emitextern { fn … }blocks instead oflink!. This is whatwindows-sysships.--minimal/.minimal()— like the default style but drops per-class wrappers, inherited forwarders, handle ergonomics, and free-function wrappers. Ideal for small, hand-curated binding sets (used bywindows-canvasandwindows-reactor). Mutually exclusive with--sys.
WinRT event accessors (add_*/remove_* pairs) are always collapsed into a single
auto-revoking Event wrapper, regardless of style or layout — see Event accessors
below.
Layout:
- Default — one Rust module per metadata namespace.
--flat/.flat()— a single flat list of items with no namespace modules.--package/.package()— one file per namespace plus aCargo.tomlwith per-namespace features; this is how the publishedwindows/windows-syscrates are produced. Mutually exclusive with--flat.
The two axes are independent, but only a few combinations are meaningful in practice.
Every in-repo crate pairs its style with --flat or --package; the default module
layout is for external consumers generating their own namespace-organized bindings.
| Style + layout | Purpose | Examples |
|---|---|---|
default + --flat |
Full-fidelity helper crate, one bindings file | windows-collections, windows-future |
default + --package |
The published umbrella crate | windows |
--sys + --flat |
Raw FFI helper crate, one bindings file | windows-result, windows-registry, … |
--sys + --package |
The published raw-FFI crate | windows-sys |
--minimal + --flat |
Small, hand-curated binding set | windows-core, windows-canvas, windows-reactor |
| any + modules | Namespace-per-module output for external consumers | (not used in-repo) |
--minimal + --package is never used (minimal targets small curated sets, packages
are the full API surface), and --package only ever pairs with default or --sys — the
two crates the repo publishes.
Other useful options:
--in/.input(..)/.inputs(..)— add your own.winmdfiles or directories. Use the literal"default"to include the bundled Windows metadata.--derive/.derive(..)— derive extra traits on generated types.--implement/.implement(..)— emit_Implscaffolding so you can implement WinRT interfaces in Rust (pass names/namespaces to scope it).--rustfmt/.rustfmt(..)— override the formatter used on the output.--dead-code/.dead_code()— emitpub(crate)instead ofpubso the compiler flags any binding you generated but never used.
The build.rs approach above regenerates bindings on every build and adds
windows-bindgen as a build dependency of your crate. For a published crate you
usually want the opposite: commit src/bindings.rs as ordinary source and depend
only on the tiny windows-link crate at runtime. Consumers then
build with no code generation, no metadata, and no windows-bindgen in their
dependency graph, and the exact bindings are visible in the published source and in
code review. This is how the crates in this repository that use windows-bindgen
are built.
The pattern has three parts.
1. The published crate depends only on windows-link and includes the
committed bindings:
# tickcount/Cargo.toml
[dependencies]
windows-link = "0.2"// tickcount/src/lib.rs
mod bindings;
/// Milliseconds elapsed since the system was started.
pub fn get_tick_count() -> u64 {
unsafe { bindings::GetTickCount64() }
}2. A separate, unpublished binary owns code generation. Keep it as a workspace member so it never becomes a dependency of the published crate:
# gen/Cargo.toml
[package]
name = "gen"
publish = false
[dependencies]
windows-bindgen = "0.66"// gen/src/main.rs
windows_bindgen::bindgen([
"--out", "tickcount/src/bindings.rs",
"--flat",
"--sys",
"--filter", "GetTickCount64",
]);--out is resolved relative to the current directory, so run the tool from the
workspace root:
cargo run -p gen3. A CI check keeps the committed bindings honest. Regenerate, then fail if the result differs from what's checked in:
- run: cargo run -p gen
- run: git diff --exit-codeIf someone edits the filter — or a new windows-bindgen changes its output — but
forgets to commit the regenerated file, git diff --exit-code returns non-zero and
the build fails. This repository uses exactly this arrangement: tool_bindings
regenerates each crate's bindings.rs from a .txt filter, and the
gen.yml
workflow runs the tools and rejects any resulting diff.
The remainder of this page covers how the crate is built and maintained. It is
for contributors and is not needed to use windows-bindgen.
windows-bindgen is hand-written — it is the generator the other crates depend
on, not generated itself. It reads ECMA-335 metadata through
windows-metadata; the default .winmd inputs live in
crates/libs/bindgen/default. It is driven by tool_bindings (the per-crate
.txt filters in crates/tools/bindings/src) and by tool_package (which
produces the published windows and windows-sys crates).
Verified by the dedicated test crates test_bindgen, test_rdl, and
test_clang (crates/tests/libs/{bindgen,rdl,clang}).
The generated output is shaped by two axes — style (Default / Sys /
Minimal) and layout (modules / --flat / --package) — plus a handful of
booleans (--dead-code, --implement, --derive). Historically the per-style
divergences were expressed as ad-hoc is_minimal() / is_sys() checks scattered
across the type writers, which made it hard to see which policies actually
differ between modes and easy to introduce accidental inconsistencies. This is an
ongoing effort to remove divergence that exists for no good reason and to make the
remaining, intentional differences explicit.
Current work — name the style policies (done). The individual
code-generation policies that distinguish the styles are now named predicates on
Style/Config rather than inline is_minimal() checks, so call sites read by
intent:
Style::emit_class_methods— emit per-class wrapper methods.Style::emit_inherited_forwarders— emit forwarders to inherited-interface methods.Style::emit_iterable_into_iterator— emit theIntoIteratorbridge to an inheritedIIterable<T>.Style::minimal_string_input/minimal_string_return— expose HSTRING params/returns as&str/String.Config::emit_runtime_name— emit the WinRTNAMEruntime-name constant.
These are behavior-preserving: regenerating every in-repo crate produces zero
diff. MethodNames::for_style was the precedent for this style-keyed centralization.
--dead-code visibility (centralized; broadening investigated and rejected).
The --dead-code workaround emits pub(crate) instead of pub so the compiler
flags unused bindings (rust-lang/rust#157961
means pub items in a non-public module are never linted). The duplicated
if dead_code { pub(crate) } else { pub } checks are now a single
Config::item_vis() helper, and the existing callable sites (class/WinRT/COM
methods, delegate new) route through it.
We investigated broadening the workaround to all nameable items (structs,
enums, consts, interfaces, …) to catch more dead code. This is not viable as a
blanket policy: generated items are frequently part of a curated crate's
public surface, and pub(crate) then fails to compile.
windows-reactorre-exports ~22 generated WinUI enums/structs as its public API (pub use bindings::Orientation;,Color,Thickness, …). Apub(crate)definition cannot be re-exported (E0364/E0365).windows-core'simpmodule is a macro-export surface: the exportedimplement_decl!/ interface macros reference$crate::imp::E_POINTERandE_NOINTERFACE, so those generated consts must staypub(E0603).
The generator cannot know which items a hand-written crate re-exports or
references from an exported macro, so it cannot safely demote them. The workaround
therefore stays scoped to callables (which are invoked, not re-exported as types) —
the original #4609 scope. A future opt-in (e.g. a filter directive marking the
re-exported types to keep pub) could revisit this, but it is not worth the
complexity today.
Event accessors (universal). Every WinRT add_X/remove_X accessor pair is
collapsed into a single method X<F>(handler) -> Result<EventRevoker>: the closure
is taken directly (no TypedEventHandler::new wrapper), and the returned
EventRevoker auto-calls the paired remove_X slot on drop (call .forget() or
.into_token() to opt out). This was originally gated to non---package builds —
the published windows crate kept the raw add_X/remove_X + token pattern — but
that gate has been removed so all layouts share one event-accessor shape. The _Impl
producer side is unaffected: implementing an event source still requires both
add_X and remove_X. Making this universal is a breaking change to the windows
crate's public event API (e.g. widget.Click(&TypedEventHandler::new(|s, a| { …; Ok(()) }))? becomes let _revoker = widget.Click(|s, a| { … })?;), but it removes
the last layout-driven divergence in method emission.
Name the sys policies (started). As with the minimal predicates, the recurring
is_sys() divergences are becoming named Style predicates so the FFI policy reads
by intent. Done so far:
Style::derive_std_traits— deriveDefault/Debug/PartialEq(on top of the always-emittedCopy/Clone); sys emits bare value types. Used by the WinRT value-type writers (types/struct.rs,types/enum.rs) and the Win32cpp_enum.rs.Style::emit_core_traits— emit thewindows-coretrait block (type-kind, runtime signature,NAME); sys has nowindows-coredependency so it omits them. Used bytypes/struct.rsandtypes/enum.rs.Style::emit_bare_typedef(is_sys() || is_minimal()) — emit handle structs and unscoped enums as a barepub type X = <underlying>alias rather than a newtype wrapper. This is a non-obvious cross-style policy that was previously spelled out inline (with an explanatory comment) incpp_handle.rs,cpp_enum.rs, andcpp_const.rs; naming it makes the relationship explicit and greppable.
Behavior-preserving: regenerating every in-repo crate produces zero diff. The rest of
the Win32 cpp_* family still uses inline is_sys() checks where the condition is
compound and site-specific (struct copyability/Drop wrapping, flag ops,
link!-vs-extern function emission, raw-pointer interface representation) — a
larger, separate follow-up.
Deduplicate recurring layout/minimal idioms (done). Beyond the style predicates, two code blocks recurred verbatim across the type writers and are now single helpers:
Config::doc_hidden_in_package— the#[doc(hidden)]-when---packageattribute on raw vtbl structs, previously copied identically intotypes/interface.rs,types/cpp_interface.rs, andtypes/delegate.rs.Config::write_value_name_const— theRuntimeType::NAMEconstant for value types (skipped in minimal mode), previously duplicated — comment and all — betweentypes/struct.rsandtypes/enum.rs.
Review conclusion. A full sweep of the remaining is_sys() / is_minimal() /
is_package() / is_flat() branches confirms the cleanly-recurring, identical
divergences have now been named or deduplicated. What remains is intentional and
site-specific: structural layout dispatch (Config::write, paths.rs,
package_writer.rs), per-type-kind dependency-closure computation (the --package
cfg blocks in class/interface/cpp_interface, which differ by type kind), and
one-off behavioral differences (delegate invoke signatures, struct field
snake-casing, class Deref target, the compound cpp_* is_sys() sites noted above).
These are not duplication-for-no-reason; collapsing them further would obscure genuine
behavioral intent rather than clarify it.
Future work.
--externis a deliberate escape hatch — keep it. The--extern(Style::Sys { extern_fns: true }) option emitsexterndeclarations instead oflink!macros. It is unused in-repo because every windows-rs crate links viaraw-dylib, but #3828 added it specifically "if your build does not yet supportraw-dyliband requires implicit linking with lib files" — i.e. for external consumers on toolchains withoutraw-dylib. That PR also unified three copies of the function-signature codegen into one, so--sys, function-pointer types,link!macros, andexternblocks all share one ABI/FFI signature implementation. Removing--externwould regress that use case and is not recommended; "unused in-repo" is expected, not dead.- Default module layout. Likewise unused in-repo (every caller passes
--flator--package), but it is the original namespace-to-module output and the natural shape an external consumer generating their own bindings would want. Retiring it is a breaking change to the published API with a plausible external audience — a maintainer decision, not an in-repo cleanup. - Finish naming the sys policies. Extend the named-predicate treatment to the
remaining compound
cpp_*is_sys()sites (struct copyability/Drop, flag ops,link!-vs-externfunction emission, raw-pointer interface representation).
Above the output-mode axes sits a second, deeper fork: WinRT vs. non-WinRT
(cpp_*) code generation. There is exactly one classification point —
winmd/reader.rs keying on TypeAttributes::WindowsRuntime — and everything below
the Type enum (types/mod.rs) is shared substrate: TypeMap, TypeTree,
Signature, Param, Dependencies, and crucially remap(), which already
collapses many Win32 metadata types onto WinRT-equivalent primitives
(BSTR/HSTRING → String, HRESULT, IUnknown, GUID, IInspectable → Object,
EventRegistrationToken → i64, D2D_MATRIX_3X2_F → Matrix3x2, …).
The fork shows up as parallel Type variants — Interface/CppInterface,
Enum/CppEnum, Struct/CppStruct, Delegate/CppDelegate, plus COM-only
CppFn/CppConst/CppHandle — each forcing an N-arm match across every dispatch
site (sort_key, write_name, write, combine, …). The conceptual key is that
WinRT is COM + IInspectable + metadata signatures: a WinRT Interface is a
CppInterface plus { IInspectable base, forced-HRESULT return policy, generics, SIGNATURE, activation }. That argues for configuration, not forking.
Essential divergence (must stay distinct):
- Return policy. WinRT vtbl methods are always
HRESULTandmethod.rsalways wrapsResult; COM preserves raw return types, which is whycpp_method.rscarries aReturnHintenum (Query / ResultValue / ResultVoid / ReturnValue / ReturnStruct). This is the deepest real axis. - Generics + SIGNATURE/RuntimeType — WinRT-only.
DelegatevsCppDelegate— a WinRT delegate is a GUID'd COM interface withInvoke; a COM "delegate" is a bareextern fnpointer. Different ABI shapes; do not merge.CppFn/CppConst/CppHandle— no WinRT analog (free exports, freestanding constants,DECLARE_HANDLEnewtypes).
Incidental duplication (safe to unify — measured, smaller than first estimated).
A first pass already landed the cheap, decisive part: CppMethodOrName was deleted in
favor of a generic MethodOrName<M> shared by both interface writers, and the
slot-skip and per-method #[cfg] policies were hoisted into the shared free functions
method_is_skipped() and write_method_cfg() (so "is this slot opaque?" and "does
this method need its own gate?" now live in exactly one place each). What remains is
narrower than the original "~600–800 line" guess; a close per-section reading of the
two write() bodies gives the realistic numbers:
- Interface (
interface.rs~509 lines vscpp_interface.rs~353). Only the vtable method-pointer emission and the_Implfield/impl/trait-method iteration are truly near-identical — about 90 lines extractable into shared helpers (emit_vtbl_methods,emit_impl_methods,emit_trait_methods). The rest is genuinely platform-specific: WinRT-only RuntimeType/SIGNATURE, generics, inherited forwarders,IntoIterator/IIterable, RuntimeName (~145 lines); COM-onlyhas_unknown_basebranching,ScopedHeapnon-IUnknownwrapping, andDeref-to-base (~135 lines). - Enum (
enum.rsvscpp_enum.rs). ~60% unifiable behind a small parameterized writer (the bitwise-operator block and scalar-constant emission are effectively identical); the remainder is WinRT trait impls vs COM scoped/#[repr]attribute handling. - Struct (
struct.rsvscpp_struct.rs). Not "only WinRT extras" — only ~15% overlaps. The COM side carries unions (explicit layout,ManuallyDrop),DECLARE_HANDLEnewtypes, nested types, arch-specific#[cfg], and bespokeDefault/Clonederive logic that WinRT structs never have. Leave this fork alone.
The proposals, in priority order:
-
Extract the interface vtable/
_Impliteration and the enum overlap into shared helpers (DONE — pure refactor, output-preserving). Implemented.interface.rsnow hosts a smallMethodItemtrait (def()/dependencies(), implemented for bothMethodandCppMethod) plus three shared emitters called from both interface writers:write_vtbl_methods— the_Vtblmethod-pointer fields (slot-name allocation, the per-method#[cfg]gate with its#[cfg]-outusizefallback, and the opaque name-only slot). The one real difference — WinRT appends-> HRESULTwhile COM'swrite_abialready carries the native return — is supplied by the caller as a closure.write_impl_field_methods— thename: name::<..>,initializer entries (the opaquename: 0,fallback is shared; the turbofish args, which carry WinRT generics or branch on COM'sOFFSET, come from the caller).write_impl_trait_methods— thefn name signature;producer-trait entries (only thewrite_impl_signaturearity differs).
The per-method extern thunks (
impl_methods) were intentionally left per-side: their generics,this-extraction (OFFSETderef vsScopedHeap), andwrite_upcallshapes genuinely diverge, so a shared helper would be all parameters and no body.enum.rslikewise gainedwrite_enum_constants(the filteredpub const X = Self(v)variants) andwrite_enum_flags(theBitOr/BitAnd/*Assign/Not/containsblock); the WinRT and COM enum writers now call both, gating on their own (u32-underlying vsFlagsAttribute) guards. Net bindgen source ≈ −22 lines, but the point is single-sourcing: four near-identical blocks now live in one place each. Proven correct by regeneratingwindows/windows-sys(tool_package),windows-reactor,windows-webview, and thetool_bindingscrates to a zero-byte diff; bindgen tests + clippy clean.Deliberately not attempted (essential forks — a unified writer would be mostly feature flags):
Struct/CppStruct(COM unions/handles/nested types/arch layout),Delegate/CppDelegate(GUID'd interface vs bareextern fn), the COM-onlyCppFn/CppConst/CppHandle, and the method return model (next item). -
Return-model axis (studied — fork is mostly essential). A close reading of the caller- and producer-side return handling in
method.rs(WinRT) andcpp_method.rs(COM, driven by theReturnHintenum) shows the two return models overlap only in theResultVoid/ResultValueshapes, and that overlap is already shared:- The success-code folding lives in
windows-core:HRESULT::ok/map/and_thenall gate onis_ok()(self.0 >= 0, i.e.SUCCEEDED). The value-side mapping is centralized once inType::write_result_map(types/mod.rs) and called by both writers. The void-side fold isvcall.ok()in both. - Everything else in each model is exclusive to one side and genuinely different:
WinRT always returns
HRESULTon the vtable with the logical return appended as a trailing out-param, plus WinRT-only axes (noexcept, WinRT arrays,IReference<T>sugar, minimal&str/String). COM preserves the native vtable return and adds COM-onlyReturnHintshapes —None(raw return passes through),ReturnValue(non-HRESULTreturn + retval out-param),ReturnStruct(by-value struct via a hidden first out-param), andQuery/QueryOptional(the(REFIID, void**)QI pattern folded intoResult<T>).
Because the strategies are ~80% disjoint, a single shared
ReturnStrategyenum would be mostly non-overlapping variants and would not reduce real complexity; merging the twowrite()bodies is therefore not a worthwhile dedup — the fork is essential, and the part that should be shared already is.Success-code (
S_FALSE) finding.SUCCEEDEDcodes are correctly treated asOkon both paths (soS_FALSEis never an error), but projecting toResult<()>/Result<T>discards the specific success code — a method that returnsS_FALSEmeaningfully loses that signal. This is a shared limitation, not a fork: it applies to WinRT too (a WinRT method can returnS_FALSE, just more rarely), and there is no metadata distinguishing meaningful-success methods. The only escape hatch today is to project the rawHRESULT(COMReturnHint::None), butCppMethod::newalways classifies anHRESULTreturn asResultVoid/ResultValue, so there is currently no way to opt a specific method out on either side. The worthwhile follow-up here is therefore a feature, not a dedup: an opt-in (e.g. a filter directive, since metadata can't express it) to preserve the rawHRESULTso callers can observeS_FALSE— applicable uniformly to WinRT and COM. The deeper reason it can't be uniform is an information-capacity mismatch, not a codegen choice: a vtable methodHRESULT M([out] T*)produces up to three distinct states — a failure code, a success code (S_OKvsS_FALSE), and the out-param value — butResult<T>has only two slots (Ok(T)|Err(code)). Collapsing always drops one: theOkarm keeps the value and drops the success code; theErrarm keeps the code and drops the value. SoS_FALSEcannot ride along "for free" in aResult<T>projection.Void vs. value asymmetry. The void case is solvable losslessly:
HRESULT M()with meaningfulS_FALSEhas no out-param, so the free slot can carry the success code — project it asResult<bool>(Ok(true)=S_OK,Ok(false)=S_FALSE,Err=FAILED) instead of the lossyResultVoid→Result<()>. The value case (HRESULT M([out] T*)) is genuinely lossy and needs a heavier shape (Result<Option<T>>withS_FALSE→Ok(None), or rawHRESULT+ caller reads the out-param).Relation to the
Error::empty()"success-but-empty" hack.windows-resultalready faces the same fork for the success-with-no-value case:Error.codeis aNonZeroI32(soResult<(), Error>stays niche-sized asOption<Error>, with the zero/Okniche), which means anErrorcan never holdS_OK(0).Error::empty()therefore stores the sentinelS_EMPTY_ERROR(b"S_OK"asi32), andType::from_abifor an interface maps success HRESULT + null out-param toErr(Error::empty())— i.e. a missing object becomes an error solet x = foo()?;stays ergonomic. Note this is a policy choice for the exact ABI situation anS_FALSE-aware value method cares about: same bits on the wire (success + null out-param), butError::empty()projects it asErrwhile anS_FALSE-aware method would wantOk(None)/Ok(false). That is precisely whyS_FALSEsupport must be opt-in — it requests the other policy. The machinery already exists in one form:ReturnHint::QueryOptionalmodels "optional out-param" by writing to a caller-provided*mut Option<T>and returningResult<()>(preserving both the HRESULT-as-Resultand present/absent), so a generalized "optional/S_FALSEout-param" hint would follow an established shape rather than a new one. - The success-code folding lives in
-
COM event transform + closure-ctor (prototyped, deliberately not kept). COM already gets most WinRT ergonomics through genuinely shared code — property naming sugar (
get_X→X(),put_X→SetX()),_Impltraits, IID-basedcast, theParamconversion trait, HRESULT →Result. Two further WinRT ergonomics were prototyped for the COM path: collapsingadd_/remove_event pairs into a singleX(handler) -> Result<EventRevoker>, and giving delegate-shaped handler interfaces (IUnknownbase, singleInvoke) a closure-acceptingnew()mirroringDelegate::write. Both were reverted.The reason is the priority order: these mirrored the WinRT behavior by adding a parallel COM codegen path (
CppEventdetection + event-collapse emit, plusdelegate_method/write_closure_ctorand closure signature/upcall helpers) that duplicatedmethod.rs/delegate.rsrather than sharing it — only the runtime types (EventRevoker,DelegateBox) were reused. That is more bindgen edge cases, not fewer, and the only consumer waswindows-webview: the publishedwindows/windows-syscrates have noEventRegistrationToken-based COM events (their one COM event,ISpellChecker, uses au32cookie), and no other crate has delegate-shaped COM handlers. Mirroring-by-duplication to serve a single crate runs against the goal of fewer edge cases, sowindows-webviewkeeps its own smallevent_handler!/EventRegistrationglue instead — the complexity stays in the one crate that needs it. If these are revisited, the bar is a single shared event/delegate generator driven from both type systems, not a second copy. -
Extend remaining minimal-mode ergonomics to COM.
IntoIteratorfor COM enumerators (IEnumXxx's Next/Skip/Reset/Clone) mirroringIIterable→BufferedIterator, and broader string in/out sugar, leveraging the existingremap()substrate.