Skip to content

Commit

Permalink
Merge pull request #201 from SamSchott/memory-management
Browse files Browse the repository at this point in the history
Autorelease objects which we own
  • Loading branch information
dgelessus authored Jan 21, 2021
2 parents 8f72b2e + f959e65 commit 7b1e109
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 3 deletions.
1 change: 1 addition & 0 deletions changes/200.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Autorelease Objective-C instances when the corresponding Python instance is destroyed.
1 change: 1 addition & 0 deletions docs/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ stand alone.

get-started
type-mapping
memory-management
protocols
async
c-functions
Expand Down
53 changes: 53 additions & 0 deletions docs/how-to/memory-management.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
===========================================
Memory management for Objective-C instances
===========================================

Reference counting works differently in Objective-C compared to Python. Python
will automatically track where variables are referenced and free memory when
the reference count drops to zero whereas Objective-C uses explicit reference
counting to manage memory. The methods ``retain``, ``release`` and
``autorelease`` are used to increase and decrease the reference counts as
described in the `Apple developer documentation
<https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html>`__.
When enabling automatic reference counting (ARC), the appropriate calls for
memory management will be inserted for you at compile-time. However, since
Rubicon Objective-C operates at runtime, it cannot make use of ARC.

You won't have to manage reference counts in Python, Rubicon Objective-C will
do that work for you. It does so by tracking when you gain ownership of an
object. This is the case when you create an Objective-C instance using a method
whose name begins with "alloc", "new", "copy", or "mutableCopy". Rubicon
Objective-C will then insert a ``release`` call when the Python variable that
corresponds to the Objective-C instance is deallocated.

An exception to this is when you manually ``retain`` an object. Rubicon
Objective-C will not keep track of such retain calls and you will be
responsible to insert appropriate ``release`` calls yourself.

You will also need to pay attention to reference counting in case of **weak
references**. In Objective-C, creating a **weak reference** means that the
reference count of the object is not incremented and the object will still be
deallocated when no strong references remain. Any weak references to the object
are then set to ``nil``.

Some objects will store references to other objects as a weak reference. Such
properties will be declared in the Apple developer documentation as
"@property(weak)" or "@property(assign)". This is commonly the case for
delegates. For example, in the code below, the ``NSOutlineView`` only stores a
weak reference to the object which is assigned to its delegate property:

.. code-block:: python
from rubicon.objc import NSObject, ObjCClass
from rubicon.objc.runtime import load_library
app_kit = load_library("AppKit")
NSOutlineView = ObjCClass("NSOutlineView")
outline_view = NSOutlineView.alloc().init()
delegate = NSObject.alloc().init()
outline_view.delegate = delegate
You will need to keep a reference to the Python variable ``delegate`` so that
the corresponding Objective-C instance does not get deallocated.
40 changes: 40 additions & 0 deletions rubicon/objc/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ def __call__(self, receiver, *args, convert_args=True, convert_result=True):
# Convert result to python type if it is a instance or class pointer.
if self.restype is not None and issubclass(self.restype, objc_id):
result = ObjCInstance(result)

# Mark for release if we acquire ownership of an object. Do not autorelease here because
# we might retain a Python reference while the Obj-C reference goes out of scope.
if self.name.startswith((b'alloc', b'new', b'copy', b'mutableCopy')):
result._needs_release = True

return result


Expand Down Expand Up @@ -641,6 +647,7 @@ def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None):
self = super().__new__(cls)
super(ObjCInstance, type(self)).__setattr__(self, "ptr", object_ptr)
super(ObjCInstance, type(self)).__setattr__(self, "_as_parameter_", object_ptr)
super(ObjCInstance, type(self)).__setattr__(self, "_needs_release", False)
if isinstance(object_ptr, objc_block):
super(ObjCInstance, type(self)).__setattr__(self, "block", ObjCBlock(object_ptr))
# Store new object in the dictionary of cached objects, keyed
Expand All @@ -649,6 +656,39 @@ def __new__(cls, object_ptr, _name=None, _bases=None, _ns=None):

return self

def release(self):
"""
Manually decrement the reference count of the corresponding objc object
The objc object is sent a dealloc message when its reference count reaches 0. Calling
this method manually should not be necessary, unless the object was explicitly
``retain``\\ed before. Objects returned from ``.alloc().init...(...)`` and similar calls
are released automatically by Rubicon when the corresponding Python object is deallocated.
"""
self._needs_release = False
send_message(self, "release", restype=objc_id, argtypes=[])

def autorelease(self):
"""
Decrements the receiver’s reference count at the end of the current autorelease pool block
The objc object is sent a dealloc message when its reference count reaches 0. If called,
the object will not be released when the Python object is deallocated.
"""
self._needs_release = False
result = send_message(self, "autorelease", restype=objc_id, argtypes=[])
return ObjCInstance(result)

def __del__(self):
"""
Release the corresponding objc instance if we own it, i.e., if it was returned by
by a method starting with 'alloc', 'new', 'copy', or 'mutableCopy' and it wasn't
already explicitly released by calling :meth:`release` or :meth:`autorelease`.
"""

if self._needs_release:
send_message(self, "release", restype=objc_id, argtypes=[])

def __str__(self):
"""Get a human-readable representation of ``self``.
Expand Down
64 changes: 61 additions & 3 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,13 @@ def test_objcclass_requires_class(self):
random_obj = NSObject.alloc().init()
with self.assertRaises(ValueError):
ObjCClass(random_obj.ptr)
random_obj.release()

def test_objcmetaclass_requires_metaclass(self):
"""ObjCMetaClass only accepts metaclass pointers."""

random_obj = NSObject.alloc().init()
with self.assertRaises(ValueError):
ObjCMetaClass(random_obj.ptr)
random_obj.release()

with self.assertRaises(ValueError):
ObjCMetaClass(NSObject.ptr)
Expand All @@ -186,7 +184,6 @@ def test_objcprotocol_requires_protocol(self):
random_obj = NSObject.alloc().init()
with self.assertRaises(ValueError):
ObjCProtocol(random_obj.ptr)
random_obj.release()

def test_objcclass_superclass(self):
"""An ObjCClass's superclass can be looked up."""
Expand Down Expand Up @@ -1211,3 +1208,64 @@ def test_objcinstance_python_attribute_keep_alive(self):
# Check that these are exactly the same objects that we stored before.
self.assertEqual(id(thing.python_object_1), python_object_1_id)
self.assertEqual(id(thing.python_object_2), python_object_2_id)

def test_objcinstance_release_owned(self):

# Create an object which we own.
obj = NSObject.alloc().init()

# Check that it is marked for release.
self.assertTrue(obj._needs_release)

# Explicitly release the object.
obj.release()

# Check that we no longer need to release it.
self.assertFalse(obj._needs_release)

# Delete it and make sure that we don't segfault on garbage collection.
del obj
gc.collect()

def test_objcinstance_autorelease_owned(self):

# Create an object which we own.
obj = NSObject.alloc().init()

# Check that it is marked for release.
self.assertTrue(obj._needs_release)

# Explicitly release the object.
res = obj.autorelease()

# Check that autorelease call returned the object itself.
self.assertIs(obj, res)

# Check that we no longer need to release it.
self.assertFalse(obj._needs_release)

# Delete it and make sure that we don't segfault on garbage collection.
del obj
gc.collect()

def test_objcinstance_retain_release(self):
NSString = ObjCClass('NSString')

# Create an object which we don't own.
string = NSString.stringWithString('test')

# Check that it is not marked for release.
self.assertFalse(string._needs_release)

# Explicitly retain the object.
res = string.retain()

# Check that autorelease call returned the object itself.
self.assertIs(string, res)

# Manually release the object.
string.release()

# Delete it and make sure that we don't segfault on garbage collection.
del string
gc.collect()

0 comments on commit 7b1e109

Please sign in to comment.