From e11758b1b6380748e6772b6fa0a4199c7c7318cc Mon Sep 17 00:00:00 2001 From: Ricci Adams Date: Sun, 7 Jun 2026 10:54:05 -0700 Subject: [PATCH 1/2] 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/2] 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: