Skip to content

windows-bindgen: composable class loses its non-aggregating new() when an *inherited* overridable interface is --implemented (minimal mode) #4843

Description

@ferruia-agent-gp

Summary

Since #4585 (f892ce653, "windows-reactor improved dead code detection"), minimal-mode codegen suppresses a composable WinRT class's non-aggregating new() once one of its interfaces is --implemented:

// crates/libs/bindgen/src/types/method.rs:1035 (at acaaa37be)
// Minimal subclassing emits `compose()` instead of non-aggregating `new()`.
let suppress_new = emit_compose && config.bindgen.style.is_minimal();

with emit_compose / needs_compose computed in class.rs.

This affects master only — f892ce653 is not an ancestor of the newest tag (73), so the behaviour is not present in any published windows-bindgen release. The builder API used in the repro below is likewise master-only.

The premise — "an --implement'd composable class is only ever composed, never activated" — is a false positive when the implemented interface is an overridable inherited from a base class rather than declared on the class itself. needs_compose fires for every class that has the implemented interface in its required_interfaces, which includes the whole derived hierarchy:

// crates/libs/bindgen/src/types/class.rs:74 (at acaaa37be)
let needs_compose = config.implement.is_some_and(|imp| {
    required_interfaces
        .iter()
        .any(|i| imp.matches(i.def.type_name()))
        || ...
});

Concrete case. A consumer that authors custom automation peers implements Microsoft.UI.Xaml.IUIElementOverrides — an overridable at UIElement, the root of every WinUI control — and composes exactly one or two classes (Control, AutomationPeer) via OnCreateAutomationPeer. With --minimal --implement Microsoft.UI.Xaml.IUIElementOverrides, all ~63 controls in the generated surface flip needs_compose = true and lose new() — yet the consuming code still constructs ~60 of them via new() (StackPanel::new(), ToolTip::new(), …), which then fail to compile with E0599 — no associated function new on the struct — with rustc pointing at compose as the associated function that is available.

These controls have no IActivationFactory; the lost new() is the composable factory's null-aggregation path (IStackPanelFactory::CreateInstance(null, null)), so they cannot be marked activatable to work around it. The same over-broad needs_compose also emits ~60 compose() methods that nothing composes.

Proposed fixes (either works for us):

  1. Opt-in flag — a --keep-new flag that emits both new() and compose() for composable classes, as non-minimal mode already does. Smallest change; zero default impact. This is what we carry as a local patch today (a keep_new: bool on Bindgen, a builder method, a CLI arm, and && !config.bindgen.keep_new appended to the suppress_new gate above — Bindgen stays #[derive(Default)], so default output is byte-identical).
  2. Correctness fix — narrow needs_compose (or the suppress_new gate) so a class only loses new() when it is the class actually being composed, not merely one that inherits an overridable whose interface is implemented. This also deletes the dead compose() methods.

We'd be glad to send a PR for whichever shape you prefer — filing the issue first per CONTRIBUTING.

Crate manifest

[build-dependencies]
windows-bindgen = { git = "https://github.com/microsoft/windows-rs", rev = "acaaa37be" }

Crate code

Generation — the trigger is --minimal plus an --implement naming an interface that is overridable on a base class (IUIElementOverrides is declared on UIElement):

windows_bindgen::builder()
    .input("crates/tools/reactor/winmd")
    .input_default()
    .output("src/bindings.rs")
    .implements([
        "Microsoft.UI.Xaml.IUIElementOverrides",
    ])
    .minimal()
    .flat()
    // Any filter set pulling in the WinUI control hierarchy reproduces it — the
    // filter has no bearing on `needs_compose`, it only decides what gets emitted.
    .filters([
        "Microsoft.UI.Xaml.Controls.StackPanel",
        "Microsoft.UI.Xaml.Controls.ToolTip",
    ])
    .write();

Consumption — every composable class that merely inherits the overridable has lost new():

// `StackPanel` is never composed; it is only ever activated.
// Before #4585 this compiled; with --minimal --implement IUIElementOverrides it does not.
let panel = StackPanel::new()?;
let tip = ToolTip::new()?;

Expected: StackPanel::new() still generated, because nothing composes StackPanel.
Actual: new() suppressed for the whole derived hierarchy; only compose() is emitted.

Minimal repro shape: --minimal --flat --implement <Overridable> on a composable class that has <Overridable> in its required-interface set → that class's new() is suppressed even though nothing composes it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions