From e11758b1b6380748e6772b6fa0a4199c7c7318cc Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Sun, 7 Jun 2026 10:54:05 -0700 Subject: [PATCH 1/7] Convert macOS backend to ARC --- src/_macosx.m | 299 ++++++++++++++++++++++-------------------------- src/meson.build | 1 + 2 files changed, 138 insertions(+), 162 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 0de0540018a7..8ce87b294a90 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -47,9 +47,24 @@ } -/* Variable for our delegate since it has a +1 reference count. - Not needed under manual reference count, but standard practice - under ARC. */ +/* This file is compiled with Automatic Reference Counting (ARC). + We need to store Objective-C objects into C structs allocated with tp_alloc(). + Support for ARC'd C struct members was not added until macOS 10.14 / Xcode 10. + As we support macOS 10.12, the following rules apply: + + 1) Each Obj-C struct member should use the __unsafe_unretained attribute. + 2) To store or update an object in a C struct, use: + STORE_OBJC_OBJECT(self->theObject, localVariable); + 3) In a tp_dealloc callback, use: + STORE_OBJC_OBJECT(self->theObject, nil); +*/ +#define STORE_OBJC_OBJECT(LOCATION, OBJECT) \ + if (LOCATION) CFRelease((__bridge void *)(LOCATION)); \ + if (OBJECT) CFRetain((__bridge void *)(id)(OBJECT)); \ + (LOCATION) = (OBJECT); + + +/* Variable for our delegate since it needs a +1 reference count. */ static id appDelegate = nil; /* Keep track of number of windows present @@ -175,25 +190,20 @@ - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app; @end @interface Window : NSWindow -{ PyObject* manager; -} -- (Window*)initWithContentRect:(NSRect)rect styleMask:(unsigned int)mask backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation withManager: (PyObject*)theManager; - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen; - (BOOL)closeButtonPressed; +@property (nonatomic, assign) PyObject* manager; @end @interface View : NSView -{ PyObject* canvas; - NSRect rubberband; +{ NSRect rubberband; @public double device_scale; } -- (void)dealloc; - (void)drawRect:(NSRect)rect; - (void)updateDevicePixelRatio:(double)scale; - (void)windowDidChangeBackingProperties:(NSNotification*)notification; - (void)windowDidResize:(NSNotification*)notification; -- (View*)initWithFrame:(NSRect)rect; -- (void)setCanvas: (PyObject*)newCanvas; +- (instancetype)initWithFrame:(NSRect)rect; - (void)windowWillClose:(NSNotification*)notification; - (BOOL)windowShouldClose:(NSNotification*)notification; - (void)mouseEntered:(NSEvent*)event; @@ -216,6 +226,7 @@ - (void)keyUp:(NSEvent*)event; - (void)scrollWheel:(NSEvent *)event; - (BOOL)acceptsFirstResponder; - (void)flagsChanged:(NSEvent*)event; +@property (nonatomic, assign) PyObject* canvas; @end /* ---------------------------- Python classes ---------------------------- */ @@ -303,10 +314,10 @@ static void lazy_init(void) { object: fh queue: nil usingBlock: ^(NSNotification* note) { + NSFileHandle* strongFileHandle __attribute__((unused)) = fh; PyGILState_STATE gstate = PyGILState_Ensure(); PyErr_CheckSignals(); PyGILState_Release(gstate); - [fh release]; [[NSNotificationCenter defaultCenter] removeObserver:notificationID]; }]; [fh waitForDataInBackgroundAndNotify]; @@ -390,7 +401,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) typedef struct { PyObject_HEAD - View* view; + __unsafe_unretained View* view; } FigureCanvas; static PyTypeObject FigureCanvasType; @@ -402,8 +413,6 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) lazy_init(); FigureCanvas *self = (FigureCanvas*)type->tp_alloc(type, 0); - if (!self) { return NULL; } - self->view = [View alloc]; return (PyObject*)self; END_OBJC_ENTRY @@ -414,10 +423,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureCanvas_init(FigureCanvas *self, PyObject *args, PyObject *kwds) { BEGIN_OBJC_ENTRY - if (!self->view) { - PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); - return -1; - } + View *view; + NSTrackingArea *trackingArea; PyObject *builtins = NULL, *super_obj = NULL, *super_init = NULL, @@ -436,17 +443,17 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) goto exit; } NSRect rect = NSMakeRect(0.0, 0.0, width, height); - self->view = [self->view initWithFrame: rect]; - self->view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; + view = [[View alloc] initWithFrame: rect]; + view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; int opts = (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect); - NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect: rect - options: opts - owner: self->view - userInfo: nil]; - [self->view addTrackingArea: area]; - [area release]; - [self->view setCanvas: (PyObject*)self]; + trackingArea = [[NSTrackingArea alloc] initWithRect: rect + options: opts + owner: view + userInfo: nil]; + [view addTrackingArea:trackingArea]; + [view setCanvas: (PyObject*)self]; + STORE_OBJC_OBJECT(self->view, view); exit: Py_XDECREF(super_obj); @@ -463,7 +470,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) { BEGIN_OBJC_ENTRY [self->view setCanvas: NULL]; - [self->view release]; + STORE_OBJC_OBJECT(self->view, nil); END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -472,7 +479,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureCanvas_repr(FigureCanvas* self) { return PyUnicode_FromFormat("FigureCanvas object %p wrapping NSView %p", - (void*)self, (void*)(self->view)); + (void*)self, (__bridge void*)(self->view)); } static PyObject* @@ -666,7 +673,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) typedef struct { PyObject_HEAD - Window* window; + __unsafe_unretained Window* window; } FigureManager; static PyObject* @@ -684,14 +691,10 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) } lazy_init(); - Window* window = [Window alloc]; - if (!window) { return NULL; } FigureManager *self = (FigureManager*)type->tp_alloc(type, 0); if (!self) { - [window release]; return NULL; } - self->window = window; ++FigureWindowCount; return (PyObject*)self; END_OBJC_ENTRY @@ -723,21 +726,22 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) NSRect rect = NSMakeRect( /* x */ 100, /* y */ 350, width, height); - self->window = [self->window initWithContentRect: rect - styleMask: NSWindowStyleMaskTitled - | NSWindowStyleMaskClosable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskMiniaturizable - backing: NSBackingStoreBuffered - defer: YES - withManager: (PyObject*)self]; - Window* window = self->window; + Window* window = [[Window alloc] initWithContentRect: rect + styleMask: NSWindowStyleMaskTitled + | NSWindowStyleMaskClosable + | NSWindowStyleMaskResizable + | NSWindowStyleMaskMiniaturizable + backing: NSBackingStoreBuffered + defer: YES]; [window setDelegate: view]; + [window setManager: (PyObject*)self]; [window makeFirstResponder: view]; [window setReleasedWhenClosed:NO]; [[window contentView] addSubview: view]; [view updateDevicePixelRatio: [window backingScaleFactor]]; + STORE_OBJC_OBJECT(self->window, window); + END_OBJC_ENTRY return 0; } @@ -767,16 +771,23 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureManager_repr(FigureManager* self) { return PyUnicode_FromFormat("FigureManager object %p wrapping NSWindow %p", - (void*) self, (void*)(self->window)); + (void*) self, (__bridge void*)(self->window)); } static void -FigureManager_dealloc(FigureManager* self) +FigureManager__closeAndClearWindow(FigureManager* self) { - BEGIN_OBJC_ENTRY [self->window close]; [self->window setDelegate:nil]; - [self->window release]; + [self->window setManager:NULL]; + STORE_OBJC_OBJECT(self->window, nil); +} + +static void +FigureManager_dealloc(FigureManager* self) +{ + BEGIN_OBJC_ENTRY + FigureManager__closeAndClearWindow(self); END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -803,10 +814,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureManager_destroy(FigureManager* self) { BEGIN_OBJC_ENTRY - [self->window close]; - [self->window setDelegate:nil]; - [self->window release]; - self->window = NULL; + FigureManager__closeAndClearWindow(self); // call super(self, FigureManager).destroy() - it seems we need the // explicit arguments, and just super() doesn't work in the C API. @@ -849,7 +857,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) PyErr_SetString(PyExc_RuntimeError, "Could not convert to NSString*"); return NULL; } - NSImage* image = [[[NSImage alloc] initByReferencingFile: ns_icon_path] autorelease]; + NSImage* image = [[NSImage alloc] initByReferencingFile: ns_icon_path]; if (!image) { PyErr_SetString(PyExc_RuntimeError, "Could not create NSImage*"); return NULL; @@ -977,12 +985,7 @@ - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app { @end @interface NavigationToolbar2Handler : NSObject -{ PyObject* toolbar; - NSButton* panbutton; - NSButton* zoombutton; -} -- (NavigationToolbar2Handler*)initWithToolbar:(PyObject*)toolbar; -- (void)installCallbacks:(SEL[7])actions forButtons:(NSButton*[7])buttons; +- (void)installCallbacks:(SEL[7])actions forButtons:(__strong NSButton*[7])buttons; - (void)home:(id)sender; - (void)back:(id)sender; - (void)forward:(id)sender; @@ -990,54 +993,50 @@ - (void)pan:(id)sender; - (void)zoom:(id)sender; - (void)configure_subplots:(id)sender; - (void)save_figure:(id)sender; +@property (nonatomic, assign) PyObject *toolbar; +@property (nonatomic, readonly) NSButton *panButton; +@property (nonatomic, readonly) NSButton *zoomButton; @end typedef struct { PyObject_HEAD - NSTextView* messagebox; - NavigationToolbar2Handler* handler; + __unsafe_unretained NSTextView* messagebox; + __unsafe_unretained NavigationToolbar2Handler* handler; int height; } NavigationToolbar2; @implementation NavigationToolbar2Handler -- (NavigationToolbar2Handler*)initWithToolbar:(PyObject*)theToolbar -{ - self = [self init]; - if (!self) { return nil; } - toolbar = theToolbar; - return self; -} -- (void)installCallbacks:(SEL[7])actions forButtons:(NSButton*[7])buttons +- (void)installCallbacks:(SEL[7])actions forButtons:(__strong NSButton*[7])buttons { for (int i = 0; i < 7; i++) { SEL action = actions[i]; NSButton* button = buttons[i]; [button setTarget: self]; [button setAction: action]; - if (action == @selector(pan:)) { panbutton = button; } - if (action == @selector(zoom:)) { zoombutton = button; } + if (action == @selector(pan:)) { _panButton = button; } + if (action == @selector(zoom:)) { _zoomButton = button; } } } --(void)home:(id)sender { gil_call_method(toolbar, "home"); } --(void)back:(id)sender { gil_call_method(toolbar, "back"); } --(void)forward:(id)sender { gil_call_method(toolbar, "forward"); } +-(void)home:(id)sender { gil_call_method(_toolbar, "home"); } +-(void)back:(id)sender { gil_call_method(_toolbar, "back"); } +-(void)forward:(id)sender { gil_call_method(_toolbar, "forward"); } -(void)pan:(id)sender { - if ([sender state]) { [zoombutton setState:NO]; } - gil_call_method(toolbar, "pan"); + if ([sender state]) { [_zoomButton setState:NO]; } + gil_call_method(_toolbar, "pan"); } -(void)zoom:(id)sender { - if ([sender state]) { [panbutton setState:NO]; } - gil_call_method(toolbar, "zoom"); + if ([sender state]) { [_panButton setState:NO]; } + gil_call_method(_toolbar, "zoom"); } --(void)configure_subplots:(id)sender { gil_call_method(toolbar, "configure_subplots"); } --(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } +-(void)configure_subplots:(id)sender { gil_call_method(_toolbar, "configure_subplots"); } +-(void)save_figure:(id)sender { gil_call_method(_toolbar, "save_figure"); } @end static PyObject* @@ -1045,14 +1044,7 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } { BEGIN_OBJC_ENTRY lazy_init(); - NavigationToolbar2Handler* handler = [NavigationToolbar2Handler alloc]; - if (!handler) { return NULL; } NavigationToolbar2 *self = (NavigationToolbar2*)type->tp_alloc(type, 0); - if (!self) { - [handler release]; - return NULL; - } - self->handler = handler; return (PyObject*)self; END_OBJC_ENTRY return NULL; @@ -1144,13 +1136,13 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } [buttons[i] setImagePosition: NSImageOnly]; [buttons[i] setToolTip: tooltip]; [[window contentView] addSubview: buttons[i]]; - [buttons[i] release]; - [image release]; rect.origin.x += rect.size.width + gap; } - self->handler = [self->handler initWithToolbar: (PyObject*)self]; - [self->handler installCallbacks: actions forButtons: buttons]; + NavigationToolbar2Handler *handler; + handler = [[NavigationToolbar2Handler alloc] init]; + [handler setToolbar:(PyObject*)self]; + [handler installCallbacks: actions forButtons: buttons]; NSFont* font = [NSFont systemFontOfSize: 0.0]; // rect.origin.x is now at the far right edge of the buttons @@ -1170,7 +1162,8 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } [[window contentView] addSubview: messagebox]; [[window contentView] display]; - self->messagebox = messagebox; + STORE_OBJC_OBJECT(self->handler, handler); + STORE_OBJC_OBJECT(self->messagebox, messagebox); END_OBJC_ENTRY return 0; } @@ -1179,8 +1172,9 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } NavigationToolbar2_dealloc(NavigationToolbar2 *self) { BEGIN_OBJC_ENTRY - [self->handler release]; - [self->messagebox release]; + [self->handler setToolbar:NULL]; + STORE_OBJC_OBJECT(self->handler, nil); + STORE_OBJC_OBJECT(self->messagebox, nil); END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -1284,16 +1278,6 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } } @implementation Window -- (Window*)initWithContentRect:(NSRect)rect styleMask:(unsigned int)mask backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation withManager: (PyObject*)theManager -{ - self = [super initWithContentRect: rect - styleMask: mask - backing: bufferingType - defer: deferCreation]; - manager = theManager; - Py_INCREF(manager); - return self; -} - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen { @@ -1307,7 +1291,7 @@ - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen - (BOOL)closeButtonPressed { - gil_call_method(manager, "_close_button_pressed"); + gil_call_method(_manager, "_close_button_pressed"); return YES; } @@ -1319,33 +1303,19 @@ - (void)close /* This is needed for show(), which should exit from [NSApp run] * after all windows are closed. */ - // For each new window, we have incremented the manager reference, so - // we need to bring that down during close and not just dealloc. - Py_DECREF(manager); } @end @implementation View -- (View*)initWithFrame:(NSRect)rect +- (instancetype)initWithFrame:(NSRect)rect { - self = [super initWithFrame: rect]; - rubberband = NSZeroRect; - device_scale = 1; + if (self = [super initWithFrame: rect]) { + rubberband = NSZeroRect; + device_scale = 1; + } return self; } -- (void)dealloc -{ - FigureCanvas* fc = (FigureCanvas*)canvas; - if (fc) { fc->view = NULL; } - [super dealloc]; -} - -- (void)setCanvas: (PyObject*)newCanvas -{ - canvas = newCanvas; -} - static void _buffer_release(void* info, const void* data, size_t size) { PyGILState_STATE gstate = PyGILState_Ensure(); PyBuffer_Release((Py_buffer *)info); @@ -1428,7 +1398,7 @@ -(void)drawRect:(NSRect)rect CGContextRef cr = [[NSGraphicsContext currentContext] CGContext]; - if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", "")) + if (!(renderer = PyObject_CallMethod(_canvas, "get_renderer", "")) || !(renderer_buffer = PyObject_CallMethod(renderer, "buffer_rgba", ""))) { PyErr_Print(); goto exit; @@ -1465,7 +1435,7 @@ - (void)updateDevicePixelRatio:(double)scale PyGILState_STATE gstate = PyGILState_Ensure(); device_scale = scale; - if (!(change = PyObject_CallMethod(canvas, "_set_device_pixel_ratio", "d", device_scale))) { + if (!(change = PyObject_CallMethod(_canvas, "_set_device_pixel_ratio", "d", device_scale))) { PyErr_Print(); goto exit; } @@ -1473,8 +1443,8 @@ - (void)updateDevicePixelRatio:(double)scale // Notify that there was a resize_event that took place process_event( "ResizeEvent", "{s:s, s:O}", - "name", "resize_event", "canvas", canvas); - gil_call_method(canvas, "draw_idle"); + "name", "resize_event", "canvas", _canvas); + gil_call_method(_canvas, "draw_idle"); [self setNeedsDisplay: YES]; } @@ -1504,7 +1474,7 @@ - (void)windowDidResize: (NSNotification*)notification PyGILState_STATE gstate = PyGILState_Ensure(); PyObject* result = PyObject_CallMethod( - canvas, "resize", "ii", width, height); + _canvas, "resize", "ii", width, height); if (result) Py_DECREF(result); else @@ -1517,7 +1487,7 @@ - (void)windowWillClose:(NSNotification*)notification { process_event( "CloseEvent", "{s:s, s:O}", - "name", "close_event", "canvas", canvas); + "name", "close_event", "canvas", _canvas); } - (BOOL)windowShouldClose:(NSNotification*)notification @@ -1554,7 +1524,7 @@ - (void)mouseEntered:(NSEvent *)event y = location.y * device_scale; process_event( "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", - "name", "figure_enter_event", "canvas", canvas, "x", x, "y", y, + "name", "figure_enter_event", "canvas", _canvas, "x", x, "y", y, "modifiers", mpl_modifiers(event)); } @@ -1567,7 +1537,7 @@ - (void)mouseExited:(NSEvent *)event y = location.y * device_scale; process_event( "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", - "name", "figure_leave_event", "canvas", canvas, "x", x, "y", y, + "name", "figure_leave_event", "canvas", _canvas, "x", x, "y", y, "modifiers", mpl_modifiers(event)); } @@ -1608,7 +1578,7 @@ - (void)mouseDown:(NSEvent *)event } process_event( "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:i, s:N}", - "name", "button_press_event", "canvas", canvas, "x", x, "y", y, + "name", "button_press_event", "canvas", _canvas, "x", x, "y", y, "button", button, "dblclick", dblclick, "modifiers", mpl_modifiers(event)); } @@ -1633,7 +1603,7 @@ - (void)mouseUp:(NSEvent *)event } process_event( "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", - "name", "button_release_event", "canvas", canvas, "x", x, "y", y, + "name", "button_release_event", "canvas", _canvas, "x", x, "y", y, "button", button, "modifiers", mpl_modifiers(event)); } @@ -1646,7 +1616,7 @@ - (void)mouseMoved:(NSEvent *)event y = location.y * device_scale; process_event( "MouseEvent", "{s:s, s:O, s:i, s:i, s:N, s:N}", - "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "name", "motion_notify_event", "canvas", _canvas, "x", x, "y", y, "buttons", mpl_buttons(), "modifiers", mpl_modifiers(event)); } @@ -1659,7 +1629,7 @@ - (void)mouseDragged:(NSEvent *)event y = location.y * device_scale; process_event( "MouseEvent", "{s:s, s:O, s:i, s:i, s:N, s:N}", - "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "name", "motion_notify_event", "canvas", _canvas, "x", x, "y", y, "buttons", mpl_buttons(), "modifiers", mpl_modifiers(event)); } @@ -1786,11 +1756,11 @@ - (void)keyDown:(NSEvent*)event if (s) { process_event( "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", - "name", "key_press_event", "canvas", canvas, "key", s, "x", x, "y", y); + "name", "key_press_event", "canvas", _canvas, "key", s, "x", x, "y", y); } else { process_event( "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", - "name", "key_press_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); + "name", "key_press_event", "canvas", _canvas, "key", Py_None, "x", x, "y", y); } } @@ -1804,11 +1774,11 @@ - (void)keyUp:(NSEvent*)event if (s) { process_event( "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", - "name", "key_release_event", "canvas", canvas, "key", s, "x", x, "y", y); + "name", "key_release_event", "canvas", _canvas, "key", s, "x", x, "y", y); } else { process_event( "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", - "name", "key_release_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); + "name", "key_release_event", "canvas", _canvas, "key", Py_None, "x", x, "y", y); } } @@ -1825,7 +1795,7 @@ - (void)scrollWheel:(NSEvent*)event int y = (int)round(point.y * device_scale - 1); process_event( "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", - "name", "scroll_event", "canvas", canvas, + "name", "scroll_event", "canvas", _canvas, "x", x, "y", y, "step", step, "modifiers", mpl_modifiers(event)); } @@ -1917,8 +1887,8 @@ - (void)flagsChanged:(NSEvent *)event typedef struct { PyObject_HEAD - NSTimer* timer; - + __unsafe_unretained NSTimer* timer; + BOOL shouldInvalidate; } Timer; static PyObject* @@ -1927,10 +1897,6 @@ - (void)flagsChanged:(NSEvent *)event BEGIN_OBJC_ENTRY lazy_init(); Timer* self = (Timer*)type->tp_alloc(type, 0); - if (!self) { - return NULL; - } - self->timer = NULL; return (PyObject*) self; END_OBJC_ENTRY return NULL; @@ -1940,13 +1906,24 @@ - (void)flagsChanged:(NSEvent *)event Timer_repr(Timer* self) { return PyUnicode_FromFormat("Timer object %p wrapping NSTimer %p", - (void*) self, (void*)(self->timer)); + (void*) self, (__bridge void*)(self->timer)); +} + +static void +Timer__timer_stop_impl(Timer* self) +{ + if (self->shouldInvalidate) { + [self->timer invalidate]; + self->shouldInvalidate = NO; + } + STORE_OBJC_OBJECT(self->timer, nil); } static PyObject* Timer__timer_start(Timer* self, PyObject* args) { BEGIN_OBJC_ENTRY + NSTimer *timer; NSTimeInterval interval; PyObject* py_interval = NULL, * py_single = NULL, * py_on_timer = NULL; int single; @@ -1962,20 +1939,27 @@ - (void)flagsChanged:(NSEvent *)event goto exit; } + // Stop any previous timers if start() was called multiple times + Timer__timer_stop_impl(self); + // hold a reference to the timer so we can invalidate/stop it later - self->timer = [NSTimer timerWithTimeInterval: interval - repeats: !single - block: ^(NSTimer *timer) { + timer = [NSTimer timerWithTimeInterval: interval + repeats: !single + block: ^(NSTimer *timer) { gil_call_method((PyObject*)self, "_on_timer"); if (single) { // A single-shot timer will be automatically invalidated when it fires, so // we shouldn't do it ourselves when the object is deleted. - self->timer = NULL; + self->shouldInvalidate = NO; } }]; + // Schedule the timer on the main run loop which is needed // when updating the UI from a background thread - [[NSRunLoop mainRunLoop] addTimer: self->timer forMode: NSRunLoopCommonModes]; + [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSRunLoopCommonModes]; + + STORE_OBJC_OBJECT(self->timer, timer); + self->shouldInvalidate = YES; exit: Py_XDECREF(py_interval); @@ -1985,15 +1969,6 @@ - (void)flagsChanged:(NSEvent *)event RETURN_NULL_OR_NONE } -static void -Timer__timer_stop_impl(Timer* self) -{ - if (self->timer) { - [self->timer invalidate]; - self->timer = NULL; - } -} - static PyObject* Timer__timer_stop(Timer* self) { diff --git a/src/meson.build b/src/meson.build index 8b52bf739c03..9465c54e9f18 100644 --- a/src/meson.build +++ b/src/meson.build @@ -145,6 +145,7 @@ if get_option('macosx') and host_machine.system() == 'darwin' ), dependencies: dependency('appleframeworks', modules: 'Cocoa'), override_options: ['werror=true'], + objc_args: ['-fobjc-arc'], install: true, ) endif From 8fd5e75f9dee6b37f53ef8a4e621bcb59c728622 Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Mon, 8 Jun 2026 11:02:29 -0700 Subject: [PATCH 2/7] Assume minimum version of macOS 10.14 --- .github/workflows/cibuildwheel.yml | 2 +- src/_macosx.m | 62 +++++++++--------------------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 36ec0e404f1f..4ab10fdf96b0 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -121,7 +121,7 @@ jobs: CIBW_SKIP: "*-musllinux_aarch64" CIBW_TEST_COMMAND: >- python {package}/ci/check_version_number.py - MACOSX_DEPLOYMENT_TARGET: "10.12" + MACOSX_DEPLOYMENT_TARGET: "10.14" strategy: matrix: include: diff --git a/src/_macosx.m b/src/_macosx.m index 8ce87b294a90..b41b89948afa 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -3,18 +3,10 @@ #include #include -/* Proper way to check for the OS X version we are compiling for, from - * https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/cross_development/Using/using.html - - * Renamed symbols cause deprecation warnings, so define macros for the new - * names if we are compiling on an older SDK */ -#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400 -#define NSButtonTypeMomentaryLight NSMomentaryLightButton -#define NSButtonTypePushOnPushOff NSPushOnPushOffButton -#define NSBezelStyleShadowlessSquare NSShadowlessSquareBezelStyle -#define CGContext graphicsPort -#endif +#if !__has_feature(objc_arc_fields) +#error "The macOS backend requires ARC C struct fields support (objc_arc_fields)." +#endif /* Various NSApplicationDefined event subtypes */ #define STOP_EVENT_LOOP 2 @@ -47,23 +39,6 @@ } -/* This file is compiled with Automatic Reference Counting (ARC). - We need to store Objective-C objects into C structs allocated with tp_alloc(). - Support for ARC'd C struct members was not added until macOS 10.14 / Xcode 10. - As we support macOS 10.12, the following rules apply: - - 1) Each Obj-C struct member should use the __unsafe_unretained attribute. - 2) To store or update an object in a C struct, use: - STORE_OBJC_OBJECT(self->theObject, localVariable); - 3) In a tp_dealloc callback, use: - STORE_OBJC_OBJECT(self->theObject, nil); -*/ -#define STORE_OBJC_OBJECT(LOCATION, OBJECT) \ - if (LOCATION) CFRelease((__bridge void *)(LOCATION)); \ - if (OBJECT) CFRetain((__bridge void *)(id)(OBJECT)); \ - (LOCATION) = (OBJECT); - - /* Variable for our delegate since it needs a +1 reference count. */ static id appDelegate = nil; @@ -401,7 +376,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) typedef struct { PyObject_HEAD - __unsafe_unretained View* view; + __strong View* view; } FigureCanvas; static PyTypeObject FigureCanvasType; @@ -453,7 +428,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) userInfo: nil]; [view addTrackingArea:trackingArea]; [view setCanvas: (PyObject*)self]; - STORE_OBJC_OBJECT(self->view, view); + self->view = view; exit: Py_XDECREF(super_obj); @@ -469,8 +444,9 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureCanvas_dealloc(FigureCanvas* self) { BEGIN_OBJC_ENTRY + NSLog(@"FigureCanvas_dealloc"); [self->view setCanvas: NULL]; - STORE_OBJC_OBJECT(self->view, nil); + self->view = nil; END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -673,7 +649,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) typedef struct { PyObject_HEAD - __unsafe_unretained Window* window; + __strong Window* window; } FigureManager; static PyObject* @@ -740,7 +716,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [[window contentView] addSubview: view]; [view updateDevicePixelRatio: [window backingScaleFactor]]; - STORE_OBJC_OBJECT(self->window, window); + self->window = window; END_OBJC_ENTRY return 0; @@ -780,7 +756,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [self->window close]; [self->window setDelegate:nil]; [self->window setManager:NULL]; - STORE_OBJC_OBJECT(self->window, nil); + self->window = nil; } static void @@ -1000,8 +976,8 @@ - (void)save_figure:(id)sender; typedef struct { PyObject_HEAD - __unsafe_unretained NSTextView* messagebox; - __unsafe_unretained NavigationToolbar2Handler* handler; + __strong NSTextView* messagebox; + __strong NavigationToolbar2Handler* handler; int height; } NavigationToolbar2; @@ -1162,8 +1138,8 @@ -(void)save_figure:(id)sender { gil_call_method(_toolbar, "save_figure"); } [[window contentView] addSubview: messagebox]; [[window contentView] display]; - STORE_OBJC_OBJECT(self->handler, handler); - STORE_OBJC_OBJECT(self->messagebox, messagebox); + self->handler = handler; + self->messagebox = messagebox; END_OBJC_ENTRY return 0; } @@ -1173,8 +1149,8 @@ -(void)save_figure:(id)sender { gil_call_method(_toolbar, "save_figure"); } { BEGIN_OBJC_ENTRY [self->handler setToolbar:NULL]; - STORE_OBJC_OBJECT(self->handler, nil); - STORE_OBJC_OBJECT(self->messagebox, nil); + self->handler = nil; + self->messagebox = nil; END_OBJC_ENTRY Py_TYPE(self)->tp_free((PyObject*)self); } @@ -1887,7 +1863,7 @@ - (void)flagsChanged:(NSEvent *)event typedef struct { PyObject_HEAD - __unsafe_unretained NSTimer* timer; + __strong NSTimer* timer; BOOL shouldInvalidate; } Timer; @@ -1916,7 +1892,7 @@ - (void)flagsChanged:(NSEvent *)event [self->timer invalidate]; self->shouldInvalidate = NO; } - STORE_OBJC_OBJECT(self->timer, nil); + self->timer = nil; } static PyObject* @@ -1958,7 +1934,7 @@ - (void)flagsChanged:(NSEvent *)event // when updating the UI from a background thread [[NSRunLoop mainRunLoop] addTimer: timer forMode: NSRunLoopCommonModes]; - STORE_OBJC_OBJECT(self->timer, timer); + self->timer = timer; self->shouldInvalidate = YES; exit: From 605ced1f1411742f5a3f3f94b5325cc904d8aa32 Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Tue, 9 Jun 2026 12:27:57 -0700 Subject: [PATCH 3/7] Rework window closing --- lib/matplotlib/backends/backend_macosx.py | 7 ++- src/_macosx.m | 74 ++++++++--------------- 2 files changed, 31 insertions(+), 50 deletions(-) diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 6ea437a90ca1..b8d4a4a9cc01 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -7,7 +7,7 @@ from .backend_agg import FigureCanvasAgg from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - ResizeEvent, TimerBase, _allow_interrupt) + CloseEvent, ResizeEvent, TimerBase, _allow_interrupt) class TimerMac(_macosx.Timer, TimerBase): @@ -161,7 +161,10 @@ def __init__(self, canvas, num): self.show() self.canvas.draw_idle() - def _close_button_pressed(self): + def _handle_window_will_close(self): + CloseEvent("close_event", self.canvas)._process() + + def _handle_window_should_close(self): Gcf.destroy(self) self.canvas.flush_events() diff --git a/src/_macosx.m b/src/_macosx.m index b41b89948afa..e1b0ed3e799f 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -10,7 +10,6 @@ /* Various NSApplicationDefined event subtypes */ #define STOP_EVENT_LOOP 2 -#define WINDOW_CLOSING 3 /* When calling into Objective-C from Python, wrap the calls with @@ -42,8 +41,8 @@ /* Variable for our delegate since it needs a +1 reference count. */ static id appDelegate = nil; -/* Keep track of number of windows present - Needed to know when to stop the NSApp */ +/* Variables to keep track of state and window count for show() */ +static BOOL IsRunningFromShow = NO; static long FigureWindowCount = 0; /* Keep track of modifier key states for flagsChanged @@ -166,7 +165,6 @@ - (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app; @interface Window : NSWindow - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen; -- (BOOL)closeButtonPressed; @property (nonatomic, assign) PyObject* manager; @end @@ -179,8 +177,6 @@ - (void)updateDevicePixelRatio:(double)scale; - (void)windowDidChangeBackingProperties:(NSNotification*)notification; - (void)windowDidResize:(NSNotification*)notification; - (instancetype)initWithFrame:(NSRect)rect; -- (void)windowWillClose:(NSNotification*)notification; -- (BOOL)windowShouldClose:(NSNotification*)notification; - (void)mouseEntered:(NSEvent*)event; - (void)mouseExited:(NSEvent*)event; - (void)mouseDown:(NSEvent*)event; @@ -671,7 +667,6 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) if (!self) { return NULL; } - ++FigureWindowCount; return (PyObject*)self; END_OBJC_ENTRY return NULL; @@ -717,6 +712,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [view updateDevicePixelRatio: [window backingScaleFactor]]; self->window = window; + ++FigureWindowCount; END_OBJC_ENTRY return 0; @@ -753,10 +749,16 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static void FigureManager__closeAndClearWindow(FigureManager* self) { - [self->window close]; - [self->window setDelegate:nil]; - [self->window setManager:NULL]; - self->window = nil; + if (self->window) { + [self->window close]; + [self->window setDelegate:nil]; + [self->window setManager:NULL]; + + self->window = nil; + if (--FigureWindowCount == 0 && IsRunningFromShow) { + [NSApp stop:nil]; + } + } } static void @@ -1265,21 +1267,6 @@ - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen return suggested; } -- (BOOL)closeButtonPressed -{ - gil_call_method(_manager, "_close_button_pressed"); - return YES; -} - -- (void)close -{ - [super close]; - --FigureWindowCount; - if (!FigureWindowCount) [NSApp stop: self]; - /* This is needed for show(), which should exit from [NSApp run] - * after all windows are closed. - */ -} @end @implementation View @@ -1461,32 +1448,21 @@ - (void)windowDidResize: (NSNotification*)notification - (void)windowWillClose:(NSNotification*)notification { - process_event( - "CloseEvent", "{s:s, s:O}", - "name", "close_event", "canvas", _canvas); + // A view should not be the delegate of a window, this check + // will go away with next refactor + Window *window = (Window *)[self window]; + if ([window isKindOfClass:[Window class]]) { + gil_call_method([window manager], "_handle_window_will_close"); + } } - (BOOL)windowShouldClose:(NSNotification*)notification { - NSWindow* window = [self window]; - // +[NSEvent otherEventWithType:...] is declared nullable but will not return - // nil for these constant, valid arguments; guard defensively anyway. - NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined - location: NSZeroPoint - modifierFlags: 0 - timestamp: 0.0 - windowNumber: 0 - context: nil - subtype: WINDOW_CLOSING - data1: 0 - data2: 0]; - if (event) { - [NSApp postEvent: event atStart: true]; - } - if ([window respondsToSelector: @selector(closeButtonPressed)]) { - BOOL closed = [((Window*) window) closeButtonPressed]; - /* If closed, the window has already been closed via the manager. */ - if (closed) { return NO; } + // A view should not be the delegate of a window, this check + // will go away with next refactor + Window *window = (Window *)[self window]; + if ([window isKindOfClass:[Window class]]) { + gil_call_method([window manager], "_handle_window_should_close"); } return YES; } @@ -1854,7 +1830,9 @@ - (void)flagsChanged:(NSEvent *)event } Py_BEGIN_ALLOW_THREADS + IsRunningFromShow = YES; [NSApp run]; + IsRunningFromShow = NO; Py_END_ALLOW_THREADS END_OBJC_ENTRY From dc577235c87a597fbc8285cda507d6a78185b743 Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Thu, 11 Jun 2026 20:27:21 -0700 Subject: [PATCH 4/7] Remove stray log. --- src/_macosx.m | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_macosx.m b/src/_macosx.m index e1b0ed3e799f..f5c0c313b828 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -440,7 +440,6 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) FigureCanvas_dealloc(FigureCanvas* self) { BEGIN_OBJC_ENTRY - NSLog(@"FigureCanvas_dealloc"); [self->view setCanvas: NULL]; self->view = nil; END_OBJC_ENTRY From fc570ac3b5585f6ad5a15f61054f091678ef9189 Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Sat, 13 Jun 2026 08:15:46 -0700 Subject: [PATCH 5/7] Fixing potential crash with -convertKeyEvent: --- src/_macosx.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index f5c0c313b828..1cfdecc3df7f 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -191,7 +191,7 @@ - (void)otherMouseUp:(NSEvent*)event; - (void)otherMouseDragged:(NSEvent*)event; - (void)setRubberband:(NSRect)rect; - (void)removeRubberband; -- (const char*)convertKeyEvent:(NSEvent*)event; +- (NSString*)convertKeyEvent:(NSEvent*)event; - (void)keyDown:(NSEvent*)event; - (void)keyUp:(NSEvent*)event; - (void)scrollWheel:(NSEvent *)event; @@ -1607,7 +1607,7 @@ - (void)removeRubberband rubberband = NSZeroRect; } -- (const char*)convertKeyEvent:(NSEvent*)event +- (NSString*)convertKeyEvent:(NSEvent*)event { NSMutableString* returnkey = [NSMutableString string]; if (keyChangeControl) { @@ -1694,12 +1694,12 @@ - (const char*)convertKeyEvent:(NSEvent*)event [returnkey setString: [returnkey substringToIndex:[returnkey length] - 1]]; } - return [returnkey UTF8String]; + return returnkey; } - (void)keyDown:(NSEvent*)event { - const char* s = [self convertKeyEvent: event]; + const char* s = [[self convertKeyEvent: event] UTF8String]; NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; location = [self convertPoint: location fromView: nil]; int x = location.x * device_scale, @@ -1717,7 +1717,7 @@ - (void)keyDown:(NSEvent*)event - (void)keyUp:(NSEvent*)event { - const char* s = [self convertKeyEvent: event]; + const char* s = [[self convertKeyEvent: event] UTF8String]; NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; location = [self convertPoint: location fromView: nil]; int x = location.x * device_scale, From d9fd9ba9ba2bc3a8503ea77e507faa31da2c1cbb Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Sat, 27 Jun 2026 09:17:07 -0700 Subject: [PATCH 6/7] MACOSX_DEPLOYMENT_TARGET to 10.14 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index baafe820f811..9c0a3ad67d11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -413,7 +413,7 @@ test-command = [ test-environment = "PIP_PREFER_BINARY=true" [tool.cibuildwheel.macos.environment] -MACOSX_DEPLOYMENT_TARGET = "10.12" +MACOSX_DEPLOYMENT_TARGET = "10.14" [tool.cibuildwheel.pyodide] test-requires = "pytest" From 80a432768ee560b014f35b3630a25dc7a1b58d4b Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Thu, 9 Jul 2026 20:09:28 -0700 Subject: [PATCH 7/7] Adding additional fix for #32019 - 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` --- src/_macosx.m | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 1cfdecc3df7f..5b227fa1281a 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -43,7 +43,7 @@ /* Variables to keep track of state and window count for show() */ static BOOL IsRunningFromShow = NO; -static long FigureWindowCount = 0; +static NSHashTable *FigureWindowHashTable = nil; /* Keep track of modifier key states for flagsChanged to keep track of press vs release */ @@ -116,7 +116,7 @@ static int wait_for_stdin() { // Short circuit if no windows are active // Rely on Python's input handling to manage CPU usage - // This queries the NSApp, rather than using our FigureWindowCount because that is decremented when events still + // This queries the NSApp, rather than using our FigureWindowHashTable because that is modified when events still // need to be processed to properly close the windows. @autoreleasepool { if (![[NSApp windows] count]) { @@ -711,7 +711,11 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [view updateDevicePixelRatio: [window backingScaleFactor]]; self->window = window; - ++FigureWindowCount; + + if (!FigureWindowHashTable) { + FigureWindowHashTable = [NSHashTable weakObjectsHashTable]; + } + [FigureWindowHashTable addObject:window]; END_OBJC_ENTRY return 0; @@ -752,9 +756,10 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [self->window close]; [self->window setDelegate:nil]; [self->window setManager:NULL]; + [FigureWindowHashTable removeObject:self->window]; self->window = nil; - if (--FigureWindowCount == 0 && IsRunningFromShow) { + if ([FigureWindowHashTable count] == 0 && IsRunningFromShow) { [NSApp stop:nil]; } } @@ -1816,14 +1821,12 @@ - (void)flagsChanged:(NSEvent *)event { BEGIN_OBJC_ENTRY - // Iterating over -[NSApp windows] will add the windows to the topmost + // Iterating over FigureWindowHashTable will add the windows to the topmost // autorelease pool, wrap in @autoreleasepool as -[NSApp run] is long-running. @autoreleasepool { [NSApp activateIgnoringOtherApps: YES]; - NSArray *windowsArray = [NSApp windows]; - NSEnumerator *enumerator = [windowsArray objectEnumerator]; - NSWindow *window; - while ((window = [enumerator nextObject])) { + + for (NSWindow *window in [FigureWindowHashTable allObjects]) { [window orderFront:nil]; } }