Convert macOS backend to ARC - #31855
Conversation
|
Overall this looks like a good direction to me. I would say that most people who have written/contributed to the macos backend are not objc developers, so we (at least myself) are likely just not aware of the objc conventions and patterns. Please feel free to correct things you think would make this easier to understand/follow. Your explanations so far have been really great, so thank you for that and the links for references, I've been learning myself! I think we should bump our minimum supported deployment target to 10.14 at a minimum for our new releases. It has been unsupported by Apple for nearly 5 years now. Python 3.12-13 already have a 10.13 (3.14 has a 10.15) minimum when building wheels, so we might not even be getting 10.12 support even if we're trying to declare it. Apple Silicon requires 11+ If this makes the code more reliable and easier to maintain in the future, we should do it now IMO. Moving all in on ARC and not requiring the extra manual reference counting macros looks much nicer to me. Some links on stats from other main Python discussions for reference as well, showing this is a really small number of people to support on that old of an XCode / OS. |
10.12 and 10.13 have broken installers, as well. They are installable, but require jumping through flaming hoops: There are also some new methods added in Mojave (10.14) related to View's device_scale math, so bumping to 10.14 may have some additional benefits. |
|
I spent yesterday and today trying to wrap my head around the interactions between the various event-loop-related APIs and how they interact with our use of NSRunLoop. My ultimate goal was making sure that both calls modifying There was a bit to unpack :) Ideal ArchitectureFirst, an NSView should not be the delegate of its window, as a window has strong ownership of its views. Generally, you would have something like an NSWindowController or another controller-level object own a window (strong reference) and that object be the window's delegate window (weak reference). In a larger-scale system with lots of bridged object types, you would probably have each PyModule
In our smaller-scale system, a lot of this would be overkill!
In any case, the "FigureManager" concept should own the window and be its delegate. It's not possible to have With that said, on to the latest changes: FigureWindowCountThe There needs to be a new -[View windowWillClose:]
This method was directly posting a For now, I'm calling a new -[View windowShouldClose:]
Note ⌘W is currently handled by our own key bindings and bypasses Based on code analysis, I was confused as to why Thus, the NSEvent posting in
Instead, I'm calling |
greglucas
left a comment
There was a problem hiding this comment.
This looks great to me and makes sense what is going on with all the updates. Thank you for taking the time to write it out and explain it as well.
|
@greglucas - I thought that I found a potential crash and pushed a fix, so you probably need to re-review.
Under non-ARC, this was fine. Under ARC, I couldn't remember the exact details of the lifetime extension and the documentation was unclear to me. On my machine (Sonoma, Xcode 15), the compiler transforms it into a I'm still not sure if this is guaranteed, or if |
greglucas
left a comment
There was a problem hiding this comment.
The new commit is minor and this all looks good to me still 👍
|
I've been testing on macOS 10.14 + Python 3.13 + Xcode 11.3. I haven't found any crashers with when running with NSZombies. Nor have I found any memory leaks. However, when I run
The 1 failure looks legitimate as the test is checking the count of I'll investigate. Sonoma and Tahoe had some failed test related to timeouts, but after re-running the tests in question several times I couldn't reproduce. |
|
After more investigation, the failure was: From my understanding, XPASS(strict) means that it passed, but it was expected to fail. The relevant code in I suspect that a recent change fixed this in Mojave and Catalina. In any case, I'm now confident with this PR as I have tested in 10.14, 14.7, and 26.5. |
# Conflicts: # .github/workflows/cibuildwheel.yml
You can do this yourself via keywords in the original post. You need a keyword per issue. - I've now done this for you by editing the post. Note: "Closes, A, B and C" does not work. You need "Closes A, closes B, closes C". |
- Add a weak-objects hash table to keep track of active `Window` objects. - Check `[FigureWindowHashTable count]` instead of `FigureWindowCount` - Iterate over said hash table in `show()` rather than using `NSApplication.windows`
|
The crash portion of #32019 is already fixed by this PR. I'm adding a fix for the rest of #32019. As far as I can tell, GitHub doesn't allow marking a pull request as dependent on another pull request. If there is a better way, please let me know. The root cause of #32019 is that AppKit is holding a strong reference to our However, if The updated PR replaces |
jklymak
left a comment
There was a problem hiding this comment.
I won't pretend to know what this does, but dont' see anything malicious ;-). @greglucas there have been some changes since your review. Suggest you take a quick look and then merge if happy. This seems to fix a tonne of macosx windowing bugs
@jklymak - Always happy to explain, if you ever feel the need to dive into the depths of Obj-C land ;) Note that this patch will change the minimum requirements to macOS 10.14 Mojave (released in 2018). Our previous minimum requirement was macOS 10.12 Sierra (released in 2016). I'm not sure what steps need to be taken to document this for release notes, but I'm happy to make those changes too if you point me in the right direction. |
|
I think it just needs to be in the release notes. I'm not sure our policy on supporting old OS'es but 10.12 is ancient and folks have the ability to use old matplotlib and/or other backends. I think modernizing to a 2018 release is easily justified. |
|
Thanks for taking this across the line @iccir, this is something I've been looking forward to! |
|
@greglucas - Thank you for merging! |
| test-environment = "PIP_PREFER_BINARY=true" | ||
|
|
||
| [tool.cibuildwheel.macos.environment] | ||
| MACOSX_DEPLOYMENT_TARGET = "10.13" |
PR summary
This pull request enables Automatic Reference Counting for the macOS backend.
Closes #21788
Closes #27147 (Assuming
show(block=False)is called)Closes #29076
Closes #31797
Closes #31798
Closes #32019
I'm still testing this PR on macOS 10.12, macOS 14, and macOS 26. I wanted to start the PR process early so that others could review my general direction and give opinions.
Note
I'm trying to follow existing coding styles already present in
_macosx.m. However, this pull request converts some Objective-C instance variables (ivars) to Objective-C properties, which adds an underscore to the ivar name.I'd like to convert more of these in the future, especially in View where there is a
@publicivar (ivar scoping attributes like@publichaven't been used for a very long time).If there are any concerns, or if I'm overstepping, please let me know!
ARC C Struct Compatibility
As we support compiling on macOS 10.12, we cannot use ARC objects within C structures. Support for this was added in Xcode 10 / macOS 10.14. See WWDC 2018 Session 409 for more information about this:
Archived Video (Direct 1.3GB download)
Archived Slides
As a workaround, I added a new macro called
STORE_OBJC_OBJECTwhich manually performs reference counting when storing into structs allocated with tp_alloc.The basic pattern for dealing with Python/Obj-C objects and using this macro is as follows:
If, someday, our minimum target for building matplotlib becomes macOS 10.14 / Xcode 10, then the following changes would occur:
This assumes that
tp_alloc()is usingPyType_GenericAlloc, which zero-fills allocated memory.Paired +alloc and -init
This PR correctly pairs each call to
+allocwith an immediate call to-init…. See #31798 for more information on this.Ownership Overview
Based on my previous experience working with language bridging, I think the following ownership structure makes sense:
Each PyObject should own an Obj-C object via a strong reference.
The Obj-C object, if needed, should have a weakly-assigned back-pointer to the PyObject. This back-pointer should be set in
Foo_initand must be cleared inFoo_dealloc.In modern Objective-C, you would use a
readwrite+assign@propertyfor the back-pointer variable:@property (nonatomic, readwrite, assign) PyObject *myPythonObject;or simply:
@property (nonatomic) PyObject *myPythonObject;This will automatically create a backing
_myPythonObjectivar as well as a-setMyPythonObject:method.FigureManager and Window
As mentioned in my last PR, FigureManager and Window strongly reference each other. I believe this is for historical reasons, likely to prevent a crash.
I made Window weakly-reference FigureManager to follow the pattern established in other objects and haven't experienced any issues. That said, I'm still wrapping my head around the interactions between Python and the main NSRunLoop.
I changed Window's raw
managerivar to a property. This synthesized asetManager:method and eliminated the need for declaring our own-init…method.There is a new
FigureManager__closeAndClearWindow()method called fromFigureManager_destroyandFigureManager_dealloc. It needs to be called from both since Windows are fairly heavyweight objects and we should probably release the memory as soon as they are closed. This method zeros-out back-pointers.NavigationToolbar2Handler
The
-init…method is no longer needed since the pointer toNavigationToolbar2is now areadwriteproperty.There was a potential crash if the
NavigationToolbar2was freed and thenNavigationToolbar2Handlertried to access it. The back-pointer needs to be cleared inNavigationToolbar2_deallocto prevent this.Modern practice is to place additional ivars directly on the
@implementationblock. You can also use@propertysyntax and make themreadonly. I did the latter here so we could get rid of the@interfaceivars.View
-[View initWithFrame:]needs to check that the call tosupersucceeds. In the very-rare case that-[NSView initWithFrame:]returns nil, we were trying to dereference a NULL pointer. Theif (self = [super init…]) {pattern is standard practice.It's also standard practice for
-init…methods to useinstancetypeas the return type. This is mostly to handle subclassing, so it's not actually needed here, but I figured that it was best to change it.Clearing a weak back-pointer in
-[View dealloc]isn't doing what it appears to do. These always need to be cleared by the owning object in the owning object'sdealloc/ThePyObject_deallocmethods.The rest of the changes involve removing
setCanvas:, as it is synthesized, and using the synthesized ivar for thecanvas@propertyinstead of the old directly-declared one.Timer
I fixed #29076 while I was testing Timer invalidation and ownership.
Timer__timer_stop_implhad to be moved soTimer__timer_startcould call it.AI Disclosure
tp_allocworks and if it zero-fills memory.PR checklist