Convert macOS backend to ARC by iccir · Pull Request #31855 · matplotlib/matplotlib
PR summary
This pull request enables Automatic Reference Counting for the macOS backend.
Closes #31797, #31798, and #29076
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 @public ivar (ivar scoping attributes like @public haven'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_OBJECT which 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:
typedef struct { PyObject_HEAD __unsafe_unretained MyObjCObject* myObjCObject; } MyPythonObject; static PyObject* MyPythonObject_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { BEGIN_OBJC_ENTRY MyPythonObject *self = (MyPythonObject*)type->tp_alloc(type, 0); return (PyObject*)self; END_OBJC_ENTRY return NULL; } static PyObject* MyPythonObject_init(MyPythonObject *self, PyObject *args, PyObject *kwds) { BEGIN_OBJC_ENTRY MyObjCObject *myObjCObject = [[MyObjCObject alloc] init]; [myObjCObject setPythonObject:self]; // See below section, back-pointer STORE_OBJC_OBJECT(self->myObjCObject, myObjCObject); END_OBJC_ENTRY return 0; } static void MyPythonObject_dealloc(MyPythonObject *self) { BEGIN_OBJC_ENTRY [self->myObjCObject setPythonObject:NULL]; // Clear back-pointer STORE_OBJC_OBJECT(self->myObjCObject, nil); END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); }
If, someday, our minimum target for building matplotlib becomes macOS 10.14 / Xcode 10, then the following changes would occur:
typedef struct { PyObject_HEAD __strong MyObjCObject* myObjCObject; // Now an ARC'd strong pointer } MyPythonObject; static PyObject* MyPythonObject_init(MyPythonObject *self, PyObject *args, PyObject *kwds) { … self->myObjCObject = myObjCObject; // Can assign directly! … } static void MyPythonObject_dealloc(MyPythonObject *self) { … self->myObjCObject = nil; // Needs to be nil before tp_free() … }
This assumes that tp_alloc() is using PyType_GenericAlloc, which zero-fills allocated memory.
Paired +alloc and -init
This PR correctly pairs each call to +alloc with 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 manager ivar to a property. This synthesized a setManager: method and eliminated the need for declaring our own -init… method.
There is a new FigureManager__closeAndClearWindow() method called from FigureManager_destroy and FigureManager_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 to NavigationToolbar2 is now a readwrite property.
There was a potential crash if the NavigationToolbar2 was freed and then NavigationToolbar2Handler tried to access it. The back-pointer needs to be cleared in NavigationToolbar2_dealloc to prevent this.
Modern practice is to place additional ivars directly on the @implementation block. You can also use @property syntax and make them readonly. I did the latter here so we could get rid of the @interface ivars.
View
-[View initWithFrame:] needs to check that the call to super succeeds. In the very-rare case that -[NSView initWithFrame:] returns nil, we were trying to dereference a NULL pointer. The if (self = [super init…]) { pattern is standard practice.
It's also standard practice for -init… methods to use instancetype as 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's dealloc/ThePyObject_dealloc methods.
The rest of the changes involve removing setCanvas:, as it is synthesized, and using the synthesized ivar for the canvas @property instead of the old directly-declared one.
Timer
I fixed #29076 while I was testing Timer invalidation and ownership.
Timer__timer_stop_impl had to be moved so Timer__timer_start could call it.
AI Disclosure
- I used AI to ask for advice about how
tp_allocworks and if it zero-fills memory. - No code was modified by AI, nor did I use any code suggested by AI.
PR checklist
- "closes #0000" is in the body of the PR description to link the related issue
- new and changed code is tested
- [N/A] Plotting related features are demonstrated in an example
- [N/A] New Features and API Changes are noted with a directive and release note
- [N/A] Documentation complies with general and docstring guidelines