Skip to content

Convert macOS backend to ARC#31855

Open
iccir wants to merge 2 commits into
matplotlib:mainfrom
iccir:arc
Open

Convert macOS backend to ARC#31855
iccir wants to merge 2 commits into
matplotlib:mainfrom
iccir:arc

Conversation

@iccir
Copy link
Copy Markdown
Contributor

@iccir iccir commented Jun 7, 2026

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:

  1. Each PyObject should own an Obj-C object via a strong reference.

  2. The Obj-C object, if needed, should have a weakly-assigned back-pointer to the PyObject. This back-pointer should be set in Foo_init and must be cleared in Foo_dealloc.

  3. In modern Objective-C, you would use a readwrite + assign @property for the back-pointer variable:

    @property (nonatomic, readwrite, assign) PyObject *myPythonObject;
    or simply:
    @property (nonatomic) PyObject *myPythonObject;

    This will automatically create a backing _myPythonObject ivar 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_alloc works and if it zero-fills memory.
  • No code was modified by AI, nor did I use any code suggested by AI.

PR checklist

@greglucas
Copy link
Copy Markdown
Contributor

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.
https://endoflife.date/macos

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+
https://cibuildwheel.pypa.io/en/stable/platforms/

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.
https://discuss.python.org/t/macos-version-support-policy/53715/21
https://discuss.python.org/t/moving-packaging-and-installers-to-macos-10-13-as-a-minimum/31907/8

@iccir
Copy link
Copy Markdown
Contributor Author

iccir commented Jun 8, 2026

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+ https://cibuildwheel.pypa.io/en/stable/platforms/

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.

@github-actions github-actions Bot added the CI: Run cibuildwheel Run wheel building tests on a PR label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI: Run cibuildwheel Run wheel building tests on a PR GUI: MacOSX

Projects

None yet

2 participants