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

Handle case where event source may remove event and any other added events don't fire #997

Merged
merged 7 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Tests/TestComponentCSharp/Class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ namespace winrt::TestComponentCSharp::implementation
}
winrt::event_token Class::IntPropertyChanged(EventHandler<int32_t> const& handler)
{
return _intChanged.add(handler);
auto eventToken = _intChanged.add(handler);
_lastIntChangedEventToken = eventToken;
return eventToken;
}
void Class::IntPropertyChanged(winrt::event_token const& token) noexcept
{
Expand All @@ -356,6 +358,10 @@ namespace winrt::TestComponentCSharp::implementation
{
_intChanged(*this, _int);
}
void Class::RemoveLastIntPropertyChangedHandler()
{
_intChanged.remove(_lastIntChangedEventToken);
}
void Class::CallForInt(TestComponentCSharp::ProvideInt const& provideInt)
{
_int = provideInt();
Expand Down
2 changes: 2 additions & 0 deletions src/Tests/TestComponentCSharp/Class.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace winrt::TestComponentCSharp::implementation

int32_t _int = 0;
winrt::event<Windows::Foundation::EventHandler<int32_t>> _intChanged;
winrt::event_token _lastIntChangedEventToken;
bool _bool = false;
winrt::event<Windows::Foundation::EventHandler<bool>> _boolChanged;
EnumValue _enumValue;
Expand Down Expand Up @@ -138,6 +139,7 @@ namespace winrt::TestComponentCSharp::implementation
winrt::event_token IntPropertyChanged(Windows::Foundation::EventHandler<int32_t> const& handler);
void IntPropertyChanged(winrt::event_token const& token) noexcept;
void RaiseIntChanged();
void RemoveLastIntPropertyChangedHandler();
void CallForInt(TestComponentCSharp::ProvideInt const& provideInt);
bool BoolProperty();
void BoolProperty(bool value);
Expand Down
1 change: 1 addition & 0 deletions src/Tests/TestComponentCSharp/TestComponentCSharp.idl
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ namespace TestComponentCSharp
Int32 IntProperty;
event Windows.Foundation.EventHandler<Int32> IntPropertyChanged;
void RaiseIntChanged();
void RemoveLastIntPropertyChangedHandler();
void CallForInt(ProvideInt provideInt);

Boolean BoolProperty;
Expand Down
54 changes: 54 additions & 0 deletions src/Tests/UnitTest/TestComponentCSharp_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,8 @@ public void TestEventSourceCaching()
{
bool eventCalled = false;
void Class_StaticIntPropertyChanged(object sender, int e) => eventCalled = (e == 3);
bool eventCalled2 = false;
void Class_StaticIntPropertyChanged2(object sender, int e) => eventCalled2 = (e == 3);

// Test static codegen-based EventSource caching
Class.StaticIntPropertyChanged += Class_StaticIntPropertyChanged;
Expand All @@ -2565,6 +2567,21 @@ public void TestEventSourceCaching()
GC.WaitForPendingFinalizers();
Class.StaticIntProperty = 3;
Assert.True(eventCalled);
eventCalled = false;

// Test adding another delegate to validate COM reference tracking in EventSource
Class.StaticIntPropertyChanged += Class_StaticIntPropertyChanged2;
Class.StaticIntProperty = 3;
Assert.True(eventCalled);
Assert.True(eventCalled2);
GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
eventCalled = false;
eventCalled2 = false;
Class.StaticIntPropertyChanged -= Class_StaticIntPropertyChanged;
Class.StaticIntProperty = 3;
Assert.False(eventCalled);
Assert.True(eventCalled2);

// Test dynamic WeakRef-based EventSource caching
eventCalled = false;
Expand Down Expand Up @@ -2606,6 +2623,43 @@ public EventHandlerClass(Action eventCalled)
}

public void IntPropertyChanged(object sender, int e) => eventCalled();
}

// Test scenario where events may be removed by the native event source without an unsubscribe.
[Fact]
public void TestEventRemovalByEventSource()
{
bool eventCalled = false;
void Class_IntPropertyChanged(object sender, int e) => eventCalled = (e == 3);
bool eventCalled2 = false;
void Class_IntPropertyChanged2(object sender, int e) => eventCalled2 = (e == 3);

var classInstance = new Class();
classInstance.IntPropertyChanged += Class_IntPropertyChanged;
classInstance.IntProperty = 3;
Assert.True(eventCalled);
Assert.False(eventCalled2);
eventCalled = false;
classInstance.RemoveLastIntPropertyChangedHandler();
classInstance.IntPropertyChanged += Class_IntPropertyChanged2;
classInstance.IntProperty = 3;
Assert.False(eventCalled);
Assert.True(eventCalled2);
eventCalled2 = false;

classInstance.RemoveLastIntPropertyChangedHandler();
GC.Collect(2, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
classInstance.IntPropertyChanged += Class_IntPropertyChanged;
classInstance.IntProperty = 3;
Assert.True(eventCalled);
Assert.False(eventCalled2);
eventCalled = false;

classInstance.IntPropertyChanged += Class_IntPropertyChanged2;
classInstance.IntProperty = 3;
Assert.True(eventCalled);
Assert.True(eventCalled2);
}

[Fact]
Expand Down
63 changes: 60 additions & 3 deletions src/cswinrt/strings/WinRT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ protected abstract class State : IDisposable
private readonly IntPtr obj;
private readonly int index;
private readonly System.WeakReference<State> cacheEntry;
private IntPtr eventInvokePtr;
private IntPtr referenceTrackerTargetPtr;

protected State(IntPtr obj, int index)
{
Expand Down Expand Up @@ -405,6 +407,52 @@ public void Dispose()
Dispose(true);
GC.SuppressFinalize(this);
}

public void InitalizeReferenceTracking(IntPtr ptr)
{
eventInvokePtr = ptr;
Guid iid = typeof(IReferenceTrackerTargetVftbl).GUID;
int hr = Marshal.QueryInterface(ptr, ref iid, out referenceTrackerTargetPtr);
if (hr != 0)
{
referenceTrackerTargetPtr = default;
}
else
{
// We don't want to keep ourselves alive and as long as this object
// is alive, the CCW still exists.
Marshal.Release(referenceTrackerTargetPtr);
}
}

public bool HasComReferences()
{
if (eventInvokePtr != default)
{
IUnknownVftbl vftblIUnknown = **(IUnknownVftbl**)eventInvokePtr;
vftblIUnknown.AddRef(eventInvokePtr);
uint comRefCount = vftblIUnknown.Release(eventInvokePtr);
if (comRefCount != 0)
{
return true;
}
}

if (referenceTrackerTargetPtr != default)
{
IReferenceTrackerTargetVftbl vftblReferenceTracker = **(IReferenceTrackerTargetVftbl**)referenceTrackerTargetPtr;
vftblReferenceTracker.AddRefFromReferenceTracker(referenceTrackerTargetPtr);
uint refTrackerCount = vftblReferenceTracker.ReleaseFromReferenceTracker(referenceTrackerTargetPtr);
if (refTrackerCount != 0)
{
// Note we can't tell if the reference tracker ref is pegged or not, so this is best effort where if there
// are any reference tracker references, we assume the event has references.
return true;
}
}

return false;
}
}
protected System.WeakReference<State> _state;

Expand Down Expand Up @@ -433,7 +481,11 @@ public void Subscribe(TDelegate del)
lock (this)
{
State state = null;
bool registerHandler = _state is null || !_state.TryGetTarget(out state);
bool registerHandler =
_state is null ||
!_state.TryGetTarget(out state) ||
// We have a wrapper delegate, but no longer has any references from any event source.
!state.HasComReferences();
if (registerHandler)
{
state = CreateEventState();
Expand All @@ -448,7 +500,8 @@ public void Subscribe(TDelegate del)
var marshaler = CreateMarshaler(eventInvoke);
try
{
var nativeDelegate = GetAbi(marshaler);
var nativeDelegate = GetAbi(marshaler);
state.InitalizeReferenceTracking(nativeDelegate);
ExceptionHelpers.ThrowExceptionForHR(_addHandler(_obj.ThisPtr, nativeDelegate, out state.token));
}
finally
Expand Down Expand Up @@ -552,8 +605,12 @@ public static void Create(IObjectReference obj, int index, System.WeakReference<
#if NETSTANDARD2_0
var weakRefSource = (IWeakReferenceSource)typeof(IWeakReferenceSource).GetHelperType().GetConstructor(new[] { typeof(IObjectReference) }).Invoke(new object[] { obj });
#else
var weakRefSource = (IWeakReferenceSource)(object)new WinRT.IInspectable(obj);
var weakRefSource = ((object)new WinRT.IInspectable(obj)) as IWeakReferenceSource;
#endif
if (weakRefSource == null)
{
return;
}
target = weakRefSource.GetWeakReference();
}
catch (Exception)
Expand Down
13 changes: 13 additions & 0 deletions src/cswinrt/strings/WinRT_Interop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,18 @@ internal struct IDelegateVftbl
{
public IUnknownVftbl IUnknownVftbl;
public IntPtr Invoke;
}

[Guid("64BD43F8-bFEE-4EC4-B7EB-2935158DAE21")]
internal unsafe struct IReferenceTrackerTargetVftbl
{
public global::WinRT.Interop.IUnknownVftbl IUnknownVftbl;
private void* _AddRefFromReferenceTracker;
public delegate* unmanaged[Stdcall]<IntPtr, uint> AddRefFromReferenceTracker => (delegate* unmanaged[Stdcall]<IntPtr, uint>)_AddRefFromReferenceTracker;
private void* _ReleaseFromReferenceTracker;
public delegate* unmanaged[Stdcall]<IntPtr, uint> ReleaseFromReferenceTracker => (delegate* unmanaged[Stdcall]<IntPtr, uint>)_ReleaseFromReferenceTracker;
private void* _Peg;
private void* _Unpeg;
}

}