Fix marshaling list collection types used for NotifyCollectionChangedEventArgs - #2525
Open
Manodasan Wignarajah (manodasanW) wants to merge 6 commits into
Open
Fix marshaling list collection types used for NotifyCollectionChangedEventArgs#2525Manodasan Wignarajah (manodasanW) wants to merge 6 commits into
Manodasan Wignarajah (manodasanW) wants to merge 6 commits into
Conversation
'NotifyCollectionChangedEventArgs' stores a single changed item in 'SingleItemReadOnlyList', an internal BCL type from 'System.ObjectModel' that implements 'IList'. Any managed collection raising a single item change, such as 'ObservableCollection<T>' bound from XAML, therefore marshals that type across the ABI as 'IList'. The CCW built for it does not expose 'IBindableVector', so that throws an 'InvalidCastException' before the change reaches the native side. The test covers the CCW directly, through a projected API taking a bindable vector, and through the event args themselves, which is the path the failure was reported from. It also asserts the runtime type name, so that it fails loudly rather than silently covering nothing if the BCL stops using that type. Checked in with the '[TestMethod]' attribute commented out, since it fails today. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
'NotifyCollectionChangedEventArgs' stores its changed items in one of two internal types from 'System.ObjectModel': 'SingleItemReadOnlyList' for a single item, and 'ReadOnlyList' for several. Any collection raising a change notification, such as an 'ObservableCollection<T>' bound from XAML, therefore marshals one of them across the ABI as 'IList', which needs a CCW exposing 'IBindableVector'. Neither type got one, so that threw an 'InvalidCastException' before the change reached the native side, and there was nothing the developer could do about it: the objects come from BCL code. Neither type can be discovered the usual way. The generator is given the framework reference assemblies, which carry public API surface only, so these types are absent from its input entirely, in every marshalling mode. They are therefore registered by name, with the interfaces they implement stated explicitly, and everything downstream is the same machinery any other user-defined type goes through. The names are resolved when the type map is built, so they are a compatibility contract: if one of these types is ever renamed or removed, Native AOT publishing fails with a type load error from ILC, while under the JIT the entry simply never matches. That is a loud and early failure rather than a silent one, which is the intended trade-off, and it is called out where the types are listed. Registering a type the generator cannot resolve also means the few places that assumed every user-defined type resolves now handle that: the proxy builder takes the interfaces it is given rather than reading them back off the type, component interface entries skip types that cannot be resolved (which are never component types), and looking up the Windows Runtime metadata name yields none for them (they are not projected types). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
Keep the proxy builder's original three branch shape rather than restructuring it around early returns, list the type names through a stack allocated span like the other fixed lists in the generator do, and align the wording of the new error message and doc comments with the ones next to them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
The metadata name lookup had grown three near identical ternaries, one per branch of the switch. Have the switch select the type to resolve instead, so there is a single resolve and a single 'no metadata name' path. Fold the component value type guard into one check using a property pattern, matching the style used elsewhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
The vtables for the list types that 'NotifyCollectionChangedEventArgs' stores its changed items in are keyed on names that are resolved when the type map is built, so if one of those types is ever renamed or removed in a future .NET version, Native AOT publishing fails with a type load error from ILC. Add 'CsWinRTGenerateCollectionChangedListVtables' so that can be turned off from a project, without waiting for a new CsWinRT release. Marshalling those objects then fails at runtime, which is the same behaviour as before they were added. The property is plumbed through the same way as 'CsWinRTMarshallingMode', including being part of the generator inputs hash, so toggling it regenerates the interop assembly rather than leaving a stale one behind. Name everything after the scenario these types are optimized for, rather than after where they happen to come from or how they behave, so the property and the code that backs it use the same vocabulary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
The component interface entries only ever get a type that was discovered from an actual assembly, so it always resolves. Guarding that with a 'TryResolve' turned a genuine failure into silently skipping the entries, which would hide the problem rather than report it. The types registered by name never reach this path: the interface entries are built once per vtable set, from a single representative type, which they are not. Also drop the note about the span, which explained the language construct rather than anything about this code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02be022a-e62f-48f5-bacc-805a2a6b40ae
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Sergio Pedri (Sergio0694)
requested changes
Aug 17, 2026
| // 'NotifyCollectionChangedEventArgs', which are registered by name, as they are absent from the reference | ||
| // assemblies the generator sees (see the discovery of those types). Those carry none of the attributes | ||
| // checked below, so they just take the same path as any other non-authored type. | ||
| _ = userDefinedType.TryResolve(module.RuntimeContext, out TypeDefinition? userDefinedTypeDefinition); |
There was a problem hiding this comment.
I don't like that this will potentially hide legitimate type resolution errors for other types, and also regardless of whether that flag is even set. Can we do this:
- Let's define a centralized place that exposes type signatures for all our special-cased internal types (right now it would just be these two)
- Here call
TryResolvefirst. If it fails, check if the type is in that set of special-cased internal types (that class could expose a helper or a set of all of them, so it can also automatically use the right comparer, and here we can just doContains) - If it matches, continue
- Otherwise, just call
Resolveto let it throw the original exception as it would've done before
Then we can also reuse that helper (e.g. InternalInteropReferences or something) in DiscoverCollectionChangedListTypes, by just looping over these two like:
foreach (TypeSignature typeSignature in (ReadOnlySpan<TypeSignature>)[
InternalInteropReferences.SingleItemReadOnlyList,
InternalInteropReferences.ReadOnlyList])
{
// Code here
}Something like this, so we have fewer special-cases hardcoded around. What do you think?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NotifyCollectionChangedEventArgs exposes collection types which are implemented as .NET internal types. Even though cswinrtinteropgen does an entire app analysis, it doesn't see these BCL types. But this is a common scenario someone may pass across the ABI, so we add special handling to add the two types to the proxy map table explicitly. If they aren't used, they should still get trimmed.