Skip to content

Commit 9220900

Browse files
authored
Fix issue with unsubscribing from events on non-agile objects (#2079)
* Fix issue where we maybe calling an add / remove handler function for a non agile object and thereby call the wrong vtable pointer when on a different context * Fix offset calculation * Simplify * Add new constructor and refactor * Use new constructor for events * Add test * Cleanup
1 parent 41e1f05 commit 9220900

19 files changed

+180
-104
lines changed

src/Tests/TestComponentCSharp/NonAgileClass.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,23 @@ namespace winrt::TestComponentCSharp::implementation
9191
*put_abi(handler) = make<NonAgileDelegate>().detach();
9292
vector.VectorChanged(handler);
9393
}
94+
95+
winrt::event_token NonAgileClass::CanExecuteChanged(winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable> const& handler)
96+
{
97+
return _event.add(handler);
98+
}
99+
100+
void NonAgileClass::CanExecuteChanged(winrt::event_token const& token) noexcept
101+
{
102+
_event.remove(token);
103+
}
104+
105+
bool NonAgileClass::CanExecute(winrt::Windows::Foundation::IInspectable const& parameter)
106+
{
107+
return true;
108+
}
109+
110+
void NonAgileClass::Execute(winrt::Windows::Foundation::IInspectable const& parameter)
111+
{
112+
}
94113
}

src/Tests/TestComponentCSharp/NonAgileClass.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,23 @@ namespace winrt::TestComponentCSharp::implementation
55
{
66
struct NonAgileClass : NonAgileClassT<NonAgileClass, winrt::non_agile>
77
{
8+
winrt::event<Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>> _event;
89
public:
910
NonAgileClass();
1011
void Observe(Microsoft::UI::Xaml::Interop::IBindableObservableVector vector);
1112
void VectorChanged(Microsoft::UI::Xaml::Interop::IBindableObservableVector vector, Windows::Foundation::IInspectable e);
13+
14+
winrt::event_token CanExecuteChanged(winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable> const& handler);
15+
void CanExecuteChanged(winrt::event_token const& token) noexcept;
16+
bool CanExecute(winrt::Windows::Foundation::IInspectable const& parameter);
17+
void Execute(winrt::Windows::Foundation::IInspectable const& parameter);
18+
19+
// Overriding runtime class name to allow marshaling as just the ICommand interface during proxy calls
20+
// to avoid defining proxy stubs for this dll.
21+
hstring GetRuntimeClassName() const
22+
{
23+
return L"Windows.UI.Xaml.Input.ICommand";
24+
}
1225
};
1326
}
1427
namespace winrt::TestComponentCSharp::factory_implementation

src/Tests/TestComponentCSharp/TestComponentCSharp.idl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ namespace TestComponentCSharp
507507
}
508508

509509
[threading(sta), marshaling_behavior(standard)]
510-
runtimeclass NonAgileClass
510+
runtimeclass NonAgileClass : Microsoft.UI.Xaml.Input.ICommand
511511
{
512512
NonAgileClass();
513513
void Observe(Microsoft.UI.Xaml.Interop.IBindableObservableVector vector);

src/Tests/UnitTest/TestComponentCSharp_Tests.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3000,6 +3000,9 @@ class NonAgileClassCaller
30003000
public void AcquireObject()
30013001
{
30023002
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
3003+
nonAgileClass = new NonAgileClass();
3004+
nonAgileClass.CanExecuteChanged += NonAgileClass_Event;
3005+
30033006
nonAgileObject = new Windows.UI.Popups.PopupMenu();
30043007
nonAgileObject.Commands.Add(new Windows.UI.Popups.UICommand("test"));
30053008
nonAgileObject.Commands.Add(new Windows.UI.Popups.UICommand("test2"));
@@ -3011,6 +3014,7 @@ public void AcquireObject()
30113014

30123015
// Object gets proxied to the apartment.
30133016
Assert.Equal(2, proxyObject.Commands.Count);
3017+
30143018
agileReference.Dispose();
30153019
}
30163020

@@ -3021,6 +3025,10 @@ public void CheckValue()
30213025
proxyObject = agileReference.Get();
30223026
Assert.Equal(2, proxyObject.Commands.Count);
30233027

3028+
// Remove the event handler from a different context from what it was initially added in
3029+
// to make sure we can unsubscribe from the event via the proxy in non agile scenarios.
3030+
nonAgileClass.CanExecuteChanged -= NonAgileClass_Event;
3031+
30243032
valueAcquired.Set();
30253033
}
30263034

@@ -3031,11 +3039,16 @@ public void CallProxyObject()
30313039
Assert.ThrowsAny<System.Exception>(() => proxyObject.Commands);
30323040
}
30333041

3042+
public void NonAgileClass_Event(object sender, object e)
3043+
{
3044+
}
3045+
30343046
private Windows.UI.Popups.PopupMenu nonAgileObject;
30353047
private Windows.UI.Popups.PopupMenu proxyObject;
3036-
private AgileReference<Windows.UI.Popups.PopupMenu> agileReference, agileReference2;
3048+
private AgileReference<Windows.UI.Popups.PopupMenu> agileReference;
30373049
private readonly AutoResetEvent objectAcquired = new AutoResetEvent(false);
30383050
private readonly AutoResetEvent valueAcquired = new AutoResetEvent(false);
3051+
private NonAgileClass nonAgileClass;
30393052
}
30403053

30413054

src/WinRT.Runtime/Interop/EventHandlerEventSource.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ public EventHandlerEventSource(
3434
{
3535
}
3636

37+
public EventHandlerEventSource(
38+
IObjectReference objectReference,
39+
int vtableIndexForAddHandler)
40+
: base(objectReference, vtableIndexForAddHandler)
41+
{
42+
}
43+
3744
/// <inheritdoc/>
3845
protected override ObjectReferenceValue CreateMarshaler(EventHandler del) =>
3946
ABI.System.EventHandler.CreateMarshaler2(del);

src/WinRT.Runtime/Interop/EventHandlerEventSource{T}.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public EventHandlerEventSource(
3535
{
3636
}
3737

38+
public EventHandlerEventSource(IObjectReference objectReference, int vtableIndexForAddHandler)
39+
: base(objectReference, vtableIndexForAddHandler)
40+
{
41+
}
42+
3843
/// <inheritdoc/>
3944
protected override ObjectReferenceValue CreateMarshaler(EventHandler<T> del) =>
4045
ABI.System.EventHandler<T>.CreateMarshaler2(del);

src/WinRT.Runtime/Interop/EventSource{TDelegate}.cs

Lines changed: 89 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,57 @@ abstract unsafe class EventSource<TDelegate>
2828
where TDelegate : class, MulticastDelegate
2929
{
3030
private readonly IObjectReference _objectReference;
31-
private readonly int _index;
32-
#if NET
33-
private readonly delegate* unmanaged[Stdcall]<IntPtr, IntPtr, EventRegistrationToken*, int> _addHandler;
34-
#else
35-
private readonly delegate* unmanaged[Stdcall]<IntPtr, IntPtr, out EventRegistrationToken, int> _addHandler;
36-
#endif
37-
private readonly delegate* unmanaged[Stdcall]<IntPtr, EventRegistrationToken, int> _removeHandler;
38-
private global::System.WeakReference<object>? _state;
39-
31+
private readonly int _index;
32+
private readonly long _vtableIndexForAddHandler;
33+
#if NET
34+
private readonly delegate* unmanaged[Stdcall]<IntPtr, IntPtr, EventRegistrationToken*, int> _addHandler;
35+
#else
36+
private readonly delegate* unmanaged[Stdcall]<IntPtr, IntPtr, out EventRegistrationToken, int> _addHandler;
37+
#endif
38+
private readonly delegate* unmanaged[Stdcall]<IntPtr, EventRegistrationToken, int> _removeHandler;
39+
private global::System.WeakReference<object>? _state;
40+
41+
// The add / remove handlers given to us can be for an object which is not agile meaning we may have
42+
// a proxy which we need to call through. Due to this, we store the offset of the add handler
43+
// we are given rather than directly caching it. Then we use that offset, to determine the add and
44+
// remove handler to call based on the pointer from the current context.
45+
#if NET
46+
private delegate* unmanaged[Stdcall]<IntPtr, IntPtr, EventRegistrationToken*, int> AddHandler
47+
#else
48+
private delegate* unmanaged[Stdcall]<IntPtr, IntPtr, out EventRegistrationToken, int> AddHandler
49+
#endif
50+
{
51+
get
52+
{
53+
if (_addHandler is not null)
54+
{
55+
return _addHandler;
56+
}
57+
58+
var thisPtr = _objectReference.ThisPtr;
59+
#if NET
60+
return (*(delegate* unmanaged[Stdcall]<IntPtr, IntPtr, global::WinRT.EventRegistrationToken*, int>**)thisPtr)[_vtableIndexForAddHandler];
61+
#else
62+
return (*(delegate* unmanaged[Stdcall]<IntPtr, IntPtr, out EventRegistrationToken, int>**)thisPtr)[_vtableIndexForAddHandler];
63+
#endif
64+
}
65+
}
66+
67+
private delegate* unmanaged[Stdcall]<IntPtr, EventRegistrationToken, int> RemoveHandler
68+
{
69+
get
70+
{
71+
if (_removeHandler is not null)
72+
{
73+
return _removeHandler;
74+
}
75+
76+
var thisPtr = _objectReference.ThisPtr;
77+
// Add 1 to the offset to get remove handler from add handler offset.
78+
return (*(delegate* unmanaged[Stdcall]<IntPtr, EventRegistrationToken, int>**)thisPtr)[_vtableIndexForAddHandler + 1];
79+
}
80+
}
81+
4082
/// <summary>
4183
/// Creates a new <see cref="EventSource{TDelegate}"/> instance with the specified parameters.
4284
/// </summary>
@@ -55,10 +97,42 @@ protected EventSource(
5597
int index = 0)
5698
{
5799
_objectReference = objectReference;
58-
_addHandler = addHandler;
59-
_removeHandler = removeHandler;
60100
_index = index;
61-
_state = EventSourceCache.GetState(objectReference, index);
101+
_state = EventSourceCache.GetState(objectReference, index);
102+
103+
// If this isn't a free threaded object, we can't cache the handlers due to we
104+
// might be accessing it from a different context which would have its own add handler address
105+
// for the proxy. So caching it would end up calling the wrong vtable.
106+
// We instead use it to calculate the vtable offset for the add handler to use later.
107+
if (objectReference is IObjectReferenceWithContext)
108+
{
109+
int vtableIndexForAddHandler = 0;
110+
while ((*(void***)objectReference.ThisPtr)[vtableIndexForAddHandler] != addHandler)
111+
{
112+
vtableIndexForAddHandler++;
113+
}
114+
_vtableIndexForAddHandler = vtableIndexForAddHandler;
115+
}
116+
else
117+
{
118+
_addHandler = addHandler;
119+
_removeHandler = removeHandler;
120+
}
121+
}
122+
123+
/// <summary>
124+
/// Creates a new <see cref="EventSource{TDelegate}"/> instance with the specified parameters.
125+
/// </summary>
126+
/// <param name="objectReference">The <see cref="IObjectReference"/> instance holding the event.</param>
127+
/// <param name="vtableIndexForAddHandler">The vtable index for the add handler of the event being managed.</param>
128+
protected EventSource(
129+
IObjectReference objectReference,
130+
int vtableIndexForAddHandler)
131+
{
132+
_objectReference = objectReference;
133+
_index = vtableIndexForAddHandler;
134+
_state = EventSourceCache.GetState(objectReference, vtableIndexForAddHandler);
135+
_vtableIndexForAddHandler = vtableIndexForAddHandler;
62136
}
63137

64138
/// <summary>
@@ -116,9 +190,9 @@ public void Subscribe(TDelegate handler)
116190

117191
EventRegistrationToken token;
118192
#if NET
119-
ExceptionHelpers.ThrowExceptionForHR(_addHandler(_objectReference.ThisPtr, nativeDelegate, &token));
193+
ExceptionHelpers.ThrowExceptionForHR(AddHandler(_objectReference.ThisPtr, nativeDelegate, &token));
120194
#else
121-
ExceptionHelpers.ThrowExceptionForHR(_addHandler(_objectReference.ThisPtr, nativeDelegate, out token));
195+
ExceptionHelpers.ThrowExceptionForHR(AddHandler(_objectReference.ThisPtr, nativeDelegate, out token));
122196
#endif
123197
global::System.GC.KeepAlive(_objectReference);
124198
state.token = token;
@@ -157,7 +231,7 @@ public void Unsubscribe(TDelegate handler)
157231

158232
private void UnsubscribeFromNative(EventSourceState<TDelegate> state)
159233
{
160-
ExceptionHelpers.ThrowExceptionForHR(_removeHandler(_objectReference.ThisPtr, state.token));
234+
ExceptionHelpers.ThrowExceptionForHR(RemoveHandler(_objectReference.ThisPtr, state.token));
161235
global::System.GC.KeepAlive(_objectReference);
162236
state.Dispose();
163237
_state = null;

src/WinRT.Runtime/MatchingRefApiCompatBaseline.net8.0.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ TypesMustExist : Type 'WinRT.DynamicWindowsRuntimeCastAttribute' does not exist
1111
MembersMustExist : Member 'public System.Guid WinRT.Interop.IID.IID_IWeakReferenceSource.get()' does not exist in the reference but it does exist in the implementation.
1212
CannotRemoveAttribute : Attribute 'System.ComponentModel.EditorBrowsableAttribute' exists on 'WinRT.Marshaler<T>' in the implementation but not the reference.
1313
TypesMustExist : Type 'WinRT.MarshalGenericHelper<T>' does not exist in the reference but it does exist in the implementation.
14-
Total Issues: 12
14+
MembersMustExist : Member 'protected void ABI.WinRT.Interop.EventSource<TDelegate>..ctor(WinRT.IObjectReference, System.Int32)' does not exist in the reference but it does exist in the implementation.
15+
MembersMustExist : Member 'public void ABI.WinRT.Interop.EventHandlerEventSource<T>..ctor(WinRT.IObjectReference, System.Int32)' does not exist in the reference but it does exist in the implementation.
16+
MembersMustExist : Member 'public void ABI.WinRT.Interop.EventHandlerEventSource..ctor(WinRT.IObjectReference, System.Int32)' does not exist in the reference but it does exist in the implementation.
17+
Total Issues: 15

src/WinRT.Runtime/Projections/ICommand.net5.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,7 @@ public static unsafe EventHandlerEventSource Get_CanExecuteChanged2(IObjectRefer
6767
{
6868
return CanExecuteChanged.GetValue(thisObj, (key) =>
6969
{
70-
var ThisPtr = obj.ThisPtr;
71-
72-
return new EventHandlerEventSource(obj,
73-
(*(delegate* unmanaged[Stdcall]<IntPtr, IntPtr, global::WinRT.EventRegistrationToken*, int>**)ThisPtr)[6],
74-
(*(delegate* unmanaged[Stdcall]<IntPtr, global::WinRT.EventRegistrationToken, int>**)ThisPtr)[7]);
70+
return new EventHandlerEventSource(obj, 6);
7571
});
7672
}
7773
}

src/WinRT.Runtime/Projections/ICommand.netstandard2.0.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,7 @@ public ICommand(IObjectReference obj) : this(obj.As<Vftbl>()) { }
146146
public ICommand(ObjectReference<Vftbl> obj)
147147
{
148148
_obj = obj;
149-
150-
_CanExecuteChanged =
151-
new EventHandlerEventSource(_obj,
152-
_obj.Vftbl.add_CanExecuteChanged_0,
153-
_obj.Vftbl.remove_CanExecuteChanged_1);
149+
_CanExecuteChanged = new EventHandlerEventSource(_obj, 6);
154150
}
155151

156152

0 commit comments

Comments
 (0)