Skip to content

Commit 2268a81

Browse files
authored
[AppKit] Try to make NSWindow.ReleasedWhenClosed work. Fixes #8606. (#17105)
NSWindow.ReleasedWhenClosed is a rather annoying API, because it interferes with reference counting. In effect it behaves kind of like autoreleasing does, except that the object (window) is released at a different time (when the window is closed, as opposed to when the autorelease pool drains). We've tried to fix this in the past in several ways: * Forcefully disable ReleasedWhenClosed in all the constructors, and if it's set in the Close method, then add an extra Retain call to offset the imminent extra release. The unfortunate side-effect is that we also call Dispose, which might be too early (see #8606). * Rewrite the code to correctly override the native 'close' method, so we get the supposedly correct semantics (our special Close code is called) when the window is closed by Objective-C (for instance when the user hits the red X to close the window). This doesn't really solve the previous problem (we're calling Dispose too early), and it doesn't work for non-subclassed NSWindows (see #8717). So I'm trying another approach: track the value of ReleasedWhenClosed, and call Retain/Release when the value switches between true/false. This way we don't need any special logic in the Close method. I've also: * Marked the ReleasedWhenClosed property as obsolete, and added a DangerousReleasedWhenClosed property, to match how we bind the other reference counting methods (retain -> DangerousRetain, release -> DangerousRelease, etc.). * Added a ReleaseWhenClosed(bool) method, to be called instead of the ReleasedWhenClosed property, and this method will call DangerousRetain/DangerousRelease as described above when the ReleasedWhenClosed property changes value. This new tracking behavior is opt-in for now, but will become opt-out in .NET 9, and hopefully we'll be able to make it the only behavior at some point (in .NET 10 maybe?). Fixes #8606. Fixes #8607. Ref: #8717.
1 parent efad190 commit 2268a81

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

src/AppKit/NSWindow.cs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#if !__MACCATALYST__
2525

2626
using System;
27+
using System.ComponentModel;
28+
2729
using CoreFoundation;
2830
using Foundation;
2931
using ObjCRuntime;
@@ -32,8 +34,31 @@ namespace AppKit {
3234

3335
public partial class NSWindow {
3436

37+
// Automatically set ReleaseWhenClosed=false in every constructor.
38+
[Obsolete ("Set 'TrackReleasedWhenClosed' and call 'ReleaseWhenClosed()' instead.")]
39+
[EditorBrowsable (EditorBrowsableState.Never)]
3540
public static bool DisableReleasedWhenClosedInConstructor;
3641

42+
// * If 'releasedWhenClosed' is true at the end of the constructor, automatically call 'retain'.
43+
// * Enable our own managed 'ReleaseWhenClosed (bool)' method (which will call 'retain'/'release' as needed).
44+
// * Don't do anything in Close (most importantly do not call Dispose).
45+
// If 'TrackReleasedWhenClosed' is not set, we default to 'false' until .NET 9, and then we default to 'true'.
46+
// Hopefully we can then remove the variable and just implement the tracking as the only behavior at some point
47+
// (XAMCORE_5_0 && NET10_0?)
48+
static bool? track_relased_when_closed;
49+
public static bool TrackReleasedWhenClosed {
50+
get {
51+
#if NET9_0
52+
return track_relased_when_closed != false;
53+
#else
54+
return track_relased_when_closed == true;
55+
#endif
56+
}
57+
set {
58+
track_relased_when_closed = value;
59+
}
60+
}
61+
3762
static IntPtr selInitWithWindowRef = Selector.GetHandle ("initWithWindowRef:");
3863

3964
// Do not actually export because NSObjectFlag is not exportable.
@@ -47,25 +72,57 @@ private NSWindow (IntPtr windowRef, NSObjectFlag x) : base (NSObjectFlag.Empty)
4772
} else {
4873
Handle = ObjCRuntime.Messaging.IntPtr_objc_msgSendSuper (this.SuperHandle, selInitWithWindowRef);
4974
}
50-
if (!DisableReleasedWhenClosedInConstructor)
51-
ReleasedWhenClosed = false;
75+
InitializeReleasedWhenClosed ();
5276
}
5377

5478
static public NSWindow FromWindowRef (IntPtr windowRef)
5579
{
5680
return new NSWindow (windowRef, NSObjectFlag.Empty);
5781
}
5882

83+
void InitializeReleasedWhenClosed ()
84+
{
85+
if (TrackReleasedWhenClosed) {
86+
if (DangerousReleasedWhenClosed)
87+
DangerousRetain ();
88+
} else if (!DisableReleasedWhenClosedInConstructor) {
89+
DangerousReleasedWhenClosed = false;
90+
}
91+
}
92+
93+
// Call DangerousRetain when 'ReleasedWhenClosed' changes from false to true.
94+
// Call DangerousRelease when 'ReleasedWhenClosed' changes from true to false.
95+
public void ReleaseWhenClosed (bool value = true)
96+
{
97+
if (!TrackReleasedWhenClosed)
98+
throw new InvalidOperationException ($"The NSWindow.{nameof (TrackReleasedWhenClosed)} field must be set to 'true' when calling this method.");
99+
100+
if (DangerousReleasedWhenClosed == value)
101+
return;
102+
103+
if (value) {
104+
DangerousRetain ();
105+
} else {
106+
DangerousRelease ();
107+
}
108+
DangerousReleasedWhenClosed = value;
109+
}
110+
59111
public void Close ()
60112
{
113+
if (TrackReleasedWhenClosed) {
114+
_Close ();
115+
return;
116+
}
117+
61118
// Windows that do not have a WindowController use released_when_closed
62119
// if set to true, the call to Close will release the object, and we will
63120
// end up with a double free.
64121
//
65122
// If that is the case, we take a reference first, and to keep the behavior
66123
// we call Dispose after that.
67124
if (WindowController == null) {
68-
bool released_when_closed = ReleasedWhenClosed;
125+
bool released_when_closed = DangerousReleasedWhenClosed;
69126
if (released_when_closed)
70127
CFObject.CFRetain (Handle);
71128
_Close ();

src/appkit.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20637,16 +20637,16 @@ partial interface NSWindow : NSAnimatablePropertyContainer, NSUserInterfaceItemI
2063720637
CGRect ContentRectFor (CGRect frameRect);
2063820638

2063920639
[Export ("init")]
20640-
[PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)]
20640+
[PostSnippet ("InitializeReleasedWhenClosed ();", Optimizable = true)]
2064120641
NativeHandle Constructor ();
2064220642

2064320643
[DesignatedInitializer]
2064420644
[Export ("initWithContentRect:styleMask:backing:defer:")]
20645-
[PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)]
20645+
[PostSnippet ("InitializeReleasedWhenClosed ();", Optimizable = true)]
2064620646
NativeHandle Constructor (CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation);
2064720647

2064820648
[Export ("initWithContentRect:styleMask:backing:defer:screen:")]
20649-
[PostSnippet ("if (!DisableReleasedWhenClosedInConstructor) { ReleasedWhenClosed = false; }", Optimizable = true)]
20649+
[PostSnippet ("InitializeReleasedWhenClosed ();", Optimizable = true)]
2065020650
NativeHandle Constructor (CGRect contentRect, NSWindowStyle aStyle, NSBackingStore bufferingType, bool deferCreation, NSScreen screen);
2065120651

2065220652
[Export ("title")]
@@ -20801,8 +20801,18 @@ partial interface NSWindow : NSAnimatablePropertyContainer, NSUserInterfaceItemI
2080120801
[Internal, Export ("close")]
2080220802
void _Close ();
2080320803

20804+
#if !XAMCORE_5_0
20805+
[Obsolete ("Call 'ReleaseWhenClosed ()' instead.")]
2080420806
[Export ("releasedWhenClosed")]
2080520807
bool ReleasedWhenClosed { [Bind ("isReleasedWhenClosed")] get; set; }
20808+
#endif
20809+
20810+
// releasedWhenClosed is a variation of sending a delayed 'autorelease', and since
20811+
// we've bound release/retain/autorelease with a 'Dangerous' prefix, we're adding
20812+
// one for this property as well.
20813+
[Sealed]
20814+
[Export ("releasedWhenClosed")]
20815+
bool DangerousReleasedWhenClosed { [Bind ("isReleasedWhenClosed")] get; set; }
2080620816

2080720817
[Export ("miniaturize:")]
2080820818
void Miniaturize ([NullAllowed] NSObject sender);

0 commit comments

Comments
 (0)