Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App-configurable compatibility options spec: CompatibilityOptions #4966

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

codendone
Copy link
Contributor

Spec for app-configurable compatibility options, which can be set via CompatibilityOptions APIs or via project properties.


# Background

_This spec adds a CompatibilityOptions class to control behavior of servicing changes._
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeCompatibilityOptions?

AppConfigurableCompatibility?

(Naming, hard :-()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - even though it's in a namespace, I'd prefer a more specific name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RuntimeCompatibilityOptions sounds good to me. I'll wait for other feedback from API review. (If we go with that, then CompatibilityChange would similarly change to RuntimeCompatibilityChange.)

specs/Compatibility/CompatibilityOptions.md Show resolved Hide resolved
features which require shared services, such as some Notification features delivered via the
Singleton package.

App-configurable compatibility is intended to prevent these problems, enabling apps to choose to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'App-configurable compatibility' - seems to be the catchy term for this feature. API naming should match?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think 'App-configurable compatibility' is useful way for us to succinctly understand the functionality of this new feature, but I thought some form of CompatibilityOptions was a better name for the API called by apps.

var compatibilityOptions = new CompatibilityOptions();
compatibilityOptions.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
compatibilityOptions.PatchMode2 = new WindowsAppRuntimeVersion(1,8,2);
compatibilityOptions.DisabledChanges.Add(CompatibilityChange.HypotheticalChange);
Copy link
Member

@DrusTheAxe DrusTheAxe Jan 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is HypotheticalChange an actual thing or it's representative of an actual value? If the latter please use a more real example #Resolved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing real I can use instead here. I could put something like TextBoxDestructionCrash or RegisterPackageRequiresUri, to name two fixes which were in 1.6.3, but then we're likely to debate those names here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that a generic symbol is appropriate, but 'hypothetical' raises my eyebrows - maybe SampleChange?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fabrikam is a fake company amd Contosso is a fake product. Do we have additional fake terms we could use? E.g. if 'top spin' is a fake feature then CompatibilityChange.BlockTopSpin or...?

```

Note that CompatibilityOptions must be applied early in the process before any other Windows App
SDK APIs are called, or right after initializing the Windows App Runtime.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializing the Windows App Runtime

What does this mean? When does this occur? There is no WindowsAppRuntimeInitialize() function (intentionally)

Do you mean the bootstrapper APIs? Other APIs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bootstrapper or other initialization code, whether from WinAppSDK auto-initializers for from explicit app calls, needs to happen first. This should happen right after that. I didn't have a better way to describe that timing that what I wrote here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "early in the process" is sufficient, given that an error would occur otherwise (the developer will learn how early is enough)

specs/Compatibility/CompatibilityOptions.md Outdated Show resolved Hide resolved
to ensure the configuration will no longer change. Calling `Apply` will also lock the
configuration.

If the configuration has already been locked, calling `Apply` will throw an `E_ILLEGAL_STATE_CHANGE`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a way to 'Reset` or 'Unapply' the configuration? OR no, 'till process death do you part?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no way to reset or unapply (unless WinAppSDK somehow gets entirely unloaded from the process). The intent is indeed 'til process death.

# API Details

```c# (but really MIDL3)
namespace Microsoft.Windows.ApplicationModel.WindowsAppRuntime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing API contract

SUGGESTION:

namespace Microsoft.Windows.ApplicationModel.WindowsAppRuntime
{
    [contractversion(1)]
    apicontract AppConfigurableCompatibilityContract{};

    /// The set of servicing changes that can be disabled.
    [contract(AppConfigurableCompatibilityContract, 1)]
    enum CompatibilityChange
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that typically in the API spec? The actual code has that, which I think should be sufficient.

UInt32 Patch;
};

/// This object is used by the app to configure any desired compatibility options
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the "app" or "process"?

Can OOP WinRT servers use this API? Are OOP WinRT servers apps?

SUGGESTION: This object is used by the process...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider the servers to be apps as well. In this place, I prefer saying "app" as something which I believe will resonate better with developers.

/// This object is used by the app to configure any desired compatibility options
/// for Windows App Runtime behavior of changes added in servicing updates. This
/// object is only used to set the runtime behavior and can't be used to query the
/// applied options.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why no query API?

Let me guess: An app specifying this knows what it's doing so it doesn't have to query at runtime.

Question: What are class libraries supposed to do?

Lacking a query API creates issues for library authors. How does a library know current process' behavior? They don't control the process/environment they run in (not the app). Would that amount to...?

[contract(AppConfigurableCompatibilityContract, 1)]
runtimeclass CurrentCompatibilityOptions
{
    static CurrentCompatibilityOptions GetDefault();

    WindowsAppRuntimeVersion EffectiveVersion { get; }

    IVector<CompatibilityChange> DisabledChanges{ get; };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Class libraries don't get to choose. Yes, the app should know what it is doing, but shouldn't have any conditional code. The intent here is that no app or class library code conditions on whether the implementation of WinAppSDK code has a fix enabled or disabled.

We could later add a query API if a good reason and need for one is found.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent here is that no app or class library code conditions on whether the implementation of WinAppSDK code has a fix enabled or disabled.

Nice in theory but as they say, the only difference between theory and practice is in theory there's no difference whereas in practice...

Some behavioral changes can't be detected (w/o dipping into version checks). Lacking a query API leaves class library developers at a loss unable to know if they need to jump left or right. This could be solved by dictating all CompatibilityChanges MUST be detectable via an IsSupported API e.g.

if (Microsoft.Stuff.Thingamajig.IsSupported("foo"))
{
    ...ComapatibilityChange.Foo is enabled...
}
else
{
    ...ComapatibilityChange.Foo is disabled or doesn't exist yet (e.g. checking in 1.7.2 when Foo was introduced in 1.7.3)...
}

If the feature doesn't already have an IsFooSupported() or IsSupported(parameter_foo) API they'll be required to add one in a patch servicing release, or if an IsSupported(parm) exists they'll need to invent a new parameter in a patch release ("easy" if parm=string, less so if enum).

This is a broad tax. It's not isolated to one feature, team or release.

A query API simplifies this problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add that if/when there is a need.


```xml
<PropertyGroup>
<WindowsAppSDKRuntimePatchMode>1.7.3</WindowsAppSDKRuntimePatchMode>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<WindowsAppSDKRuntimePatchMode>1.7.3</WindowsAppSDKRuntimePatchMode>
<WindowsAppSDKRuntimePatchMode1>1.7.3</WindowsAppSDKRuntimePatchMode1>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numeric suffixes should be consistent between xml and api

Windows App SDK team to understand the impact of the change and prioritize a fix. You can report
issues on the [WindowsAppSDK GitHub repo](https://github.com/microsoft/WindowsAppSDK/issues/new?template=bug-report.yaml).

The capability to disable a change is **not** available across new Windows App SDK stable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have automation in place (or planned), both to enforce containment of fixes, and to reset the containment "counter" for a new major.minor release?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CR and shiproom approval is the only verification we (already) have today. I think that should be sufficient.

There is no need to reset for each new major.minor release: the containment is never in the main branch and therefore never carries over to the next release.

Comment on lines 188 to 192
// Add real changes here:

// 1.7.1
// HypotheticalChange,
// OtherHypotheticalChange,
Copy link
Member

@Scottj1s Scottj1s Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we see multiple changelists being applied across patch versions, given the app only sees one version of the SDK? i.e., where would the // 1.8.3 changes be discovered?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Across patch versions within a single major.minor release? Nothing special is required for that scenario.

If you mean specifying DisabledChanges, as an example, for both the 1.7.x version and 1.8.x version the app might use, that is not possible with the proposed design. The CompatibilityChange enum will only be defined with the values for the major.minor version the app compiles against. This adds another reason DisabledChanges should be temporary.

(Technically, a developer could copy the numeric value of the CompatibilityChange from the other major.minor SDK version and directly use that value. The intent is for that to not happen, or at least be so rare that the hack can be used if needed.)


If the configuration has already been locked, calling `Apply` will throw an `E_ILLEGAL_STATE_CHANGE`
exception if the options differ from the locked configuration. It is okay to call `Apply` multiple
times with the same configuration, such as if the process had initialized and used the Windows
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a real scenario? Is WinUI finally robust enough to support it? If not, I'd just omit this sentence and allow that multiple calls are idempotent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would WinUI matter here? APIs shouldn't be calling Apply() only the app (the app/dev owns process context decisions).

E_ILLEGAL_STATE_CHANGE for 2nd+ Apply() call that would produce different outcomes (and S_OK if no different) seems righteous . Most apps will only call Apply once (and more often, nonce) but if devs make multiple calls the error seems appropriate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinUI has not supported reinitialization well (TLS cleanup issues), so better not to suggest this.

I have no problem with any subsequent call (whether identical or not) producing an E_ILLEGAL_STATE_CHANGE. The app ought to be able to manage that requirement and it frees us from complex comparisons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WinUI doesn't support it yet, but why should we make an app's attempt to ensure it is applied be an error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other scenario we've had with apps using XAML Islands is sometimes they are initializing multiple thread simultaneously. We could require apps to add a lock to ensure they only initialize once, which some apps will forget to do, or we can handle this in the Apply API using the mechanism described here. Allowing apps to simply Apply without a check is easier and safer for apps.

WindowsAppRuntimeVersion PatchMode1 { get; set; };

/// An optional patch mode to use if the runtime version matches the major.minor version.
WindowsAppRuntimeVersion PatchMode2 { get; set; };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API is hardwired presuming an app will only work with 2 versions. That doesn't scale into the future if e.g. an app works with 1.7, 1.8 and 1.9. Or when WinAppSDK has LTSC releases for an app to work with e.g. LTSC 1.7, LTSC 1.10, current 1.11 and vNext 1.12. Any change to support >2 patch modes will weird API shape changes (PatchMode3, PatchMode4... or more break-y changes)

RECOMMEND: Replace PatchMode1 and PatchMode2 with IVector<WindowsAppRuntimeVersion> PatchModes{ get; }; Then the object supports an extensible list of versions, much like it supports an extensible list of DisabledChanges.

For example, the sample on line 55 changes from

void ApplyCompatibilityOptions()
{
    var compatibilityOptions = new CompatibilityOptions();
    compatibilityOptions.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
    compatibilityOptions.PatchMode2 = new WindowsAppRuntimeVersion(1,8,2);
    compatibilityOptions.Apply();
}

to

void ApplyCompatibilityOptions()
{
    var compatibilityOptions = new CompatibilityOptions();
    compatibilityOptions.Add(new WindowsAppRuntimeVersion(1,7,3));
    compatibilityOptions.Add(new WindowsAppRuntimeVersion(1,8,2));
    compatibilityOptions.Apply();
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had suggested the list before in the code review. But it's not necessary to generalize beyond 2 - this is necessary to cover A/B Forward (otherwise one level would be sufficient).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You assume "run with product version X and test version Y". That an app will only need 2 versions, but apps can be designed to work with any of multiple versions (especially when using WinAppSDK/MSIX and not SelfContained).

CompatibilityOptions is available to all WinAppSDK users, right? Not just if WindowsAppSdkSelfContained=true?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an assumption - a hard limit. A/B Forward will only support building with major.minor and running on that and major.minor-1. CompatibilityOptions is designed primarily for FWP-dependent apps, not WindowsAppSdkSelfContained.

Copy link
Member

@DrusTheAxe DrusTheAxe Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard limit - sure. My point is, if "max 2 versions" needs >2 in the future the API shape would need impactful changes. Plus PatchMode1 and PatchMode2 property names are kinda weird. If instead it's a list it wouldn't pose future API shape issues (and not be 'weird'), and there's no significant difference to the developer experience.

P.S. "designed primarily for" == SHOULD, not MUST. That makes it less the law than the Jack Sparrow Rule (There's what a man can do, and what a man can't do...). But as John Wick taught us, a pencil can be used for more than designed primarily for... :-)

I didn't see any overt block disallowing or disabling this API if WinAppSDKSelfContained=true. Which is a good thing. Even if we expect more WinAppSDK/MSIX apps using this than WinAppSDK/SelfContained I can see value for the latter, albeit possibly less often. For instance, apps wanting some perf or functional improvements in, say, 1.42.87 but the updated Fubar feature griefs them, so use 1.42.87 but disable CompatibleOption.Fubar until MS gets a fix out in 1.42.88(+). Works equally well for SelfContained as MSIX. Flexibility good. Flexibility without downside even better :P

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It certainly feels like a better API design to use a list and enforce an element count of [0..2] (throwing an exception on element 3). I can get behind that.

I do not understand your movie analogies. But that's ok.

I say designed primarily for FWP-dependent, but not exclusively (per Michael's intro text). Yes, WindowsAppSdkSelfContained could certainly also benefit - I assume for punching holes via disabled changes, without using the patch level feature (irrelevant).

Copy link
Contributor Author

@codendone codendone Jan 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with a list for this property and I didn't like how it looked. It adds unnecessary complexity to the project properties and the runtime code. As Scott said, our A/B design is specifically between two versions.

configuration.

If the configuration has already been locked, calling `Apply` will throw an `E_ILLEGAL_STATE_CHANGE`
exception if the options differ from the locked configuration. It is okay to call `Apply` multiple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the options differ from the locked configuration

The options, or the effective options?

If an app winds up calling

var co = new CompatibilityOptions();
co.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
co.PatchMode2 = new WindowsAppRuntimeVersion(1,8,2);
co.Apply();
...
var co2 = new CompatibilityOptions();
co2.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
co2.Apply();

and it's running with 1.7 the options to Apply differ but the effective options are identical.

Is the check for identical values for all properties of the object, or identical outcomes of the runtime decision?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, seems like all kinds of interesting edge cases are possible. I vote for a strict single Apply call pattern.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strict single Apply call pattern

As in, any 2nd+ Apply() errors (regardless the 2nd+ values)?

Seems reasonable. You've got my axe :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second Apply will fail if it doesn't exactly match the first one. WinUI has long wanted to support an app being able to uninitialize and reinitialize, and this ability to call Apply again with the identical values enables that scenario. I don't see a need to prevent the scenario and make initialization potentially being more difficult for apps which manually initialize WinAppSDK.

Copy link
Contributor Author

@codendone codendone left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I forgot to submit my comments from last week.

will have Windows App SDK run in that mode even if 1.7.3 or later is installed. This capability
allows you to control when your app gets new fixes or behavior changes, even when not using
self-contained mode.
2. **Temporarily disable specific changes:** If your app encounters a problem with a specific
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference here is intentional. PatchMode doesn't necessarily mean there is a change the app wants to disable, it could just be the maximum version the app has tested against and they don't want to test against higher.

The DisabledChanges list is used for identified compatibility issues an app hits. These should either be reported to the WinAppSDK team so it can be fixed if it is a bug, or should be fixed in the app, if appropriate, since otherwise this will be a known compatibility issue when trying to move to the next significant release of WinAppSDK (such as from 1.7.2 to 1.8.x). Because of all this, the DisabledChanges list should be considered temporary, one way or the other.

specs/Compatibility/CompatibilityOptions.md Show resolved Hide resolved
{
var compatibilityOptions = new CompatibilityOptions();
compatibilityOptions.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
compatibilityOptions.PatchMode2 = new WindowsAppRuntimeVersion(1,8,2);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, see the paragraph at line 84.

var compatibilityOptions = new CompatibilityOptions();
compatibilityOptions.PatchMode1 = new WindowsAppRuntimeVersion(1,7,3);
compatibilityOptions.PatchMode2 = new WindowsAppRuntimeVersion(1,8,2);
compatibilityOptions.DisabledChanges.Add(CompatibilityChange.HypotheticalChange);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be a compile error if an unknown enum name is used.

```

Note that CompatibilityOptions must be applied early in the process before any other Windows App
SDK APIs are called, or right after initializing the Windows App Runtime.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment.


Note that disabling a change is a temporary measure which gives time for a fix to be released in a
future Windows App SDK update or for you to implement an update in your app. You should report
changes you disable if you believe it to be a bug in the Windows App SDK. This will help the
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the intent, yes. We still need users to report issues to help us understand why a change needed to be disabled.

releases (such as 1.7.x to 1.8.x), so this does not allow you to permanently disable an
intentional behavior change. Also, new stable releases, such as 1.8.0, start with no changes
available to disable -- this capability only applies to disabling targeted servicing changes
added in servicing releases.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to prefix the version, since if the app compiles against 1.8 then none of the 1.7 enum values will exist. (This gets back to the comment on why DisabledChanges are temporary.)

specs/Compatibility/CompatibilityOptions.md Show resolved Hide resolved
specs/Compatibility/CompatibilityOptions.md Show resolved Hide resolved
</PropertyGroup>
```

The `WindowsAppSDKDisabledChanges` property is a comma-separated list of `CompatibilityChange`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think whitespace-only would be less easy to read. Think of the contents of this property like specifying the contents of an array.

Copy link
Member

@DrusTheAxe DrusTheAxe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants