Objects created with alloc/init, new or mutableCopy are never freed
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 45/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- objective-c, python
- Domain
- desktop-dev
Research direction
Start at instanciate_from(ret_id) in pyobjus/pyobjus_conversions.pxi and the wrapper release path in pyobjus/pyobjus_types.pxi; trace how selector_returns_retained() is used for delegate returns. Read the Rubicon lifecycle tests named in the issue, then reproduce with the malloc counter. Done means alloc/new/copy/mutableCopy results are not over-retained, init handles a changed or nil receiver, and the lifecycle checks show deallocation after the Python wrapper is gone; the pool helpers in _runtime.h/common.pxi are a separate follow-up.
Written by the indexing model from the issue text.
Description
Summary
Objects created with alloc().init…(), new() or mutableCopy() are never freed, even after the Python wrapper is gone. Autoreleased objects are only freed if an autorelease pool is drained, and pyobjus neither drains one nor lets you create one.
import ctypes
import gc
from pyobjus import autoclass
class MallocStats(ctypes.Structure):
_fields_ = [("blocks_in_use", ctypes.c_uint),
("size_in_use", ctypes.c_size_t),
("max_size_in_use", ctypes.c_size_t),
("size_allocated", ctypes.c_size_t)]
def malloc_mb_in_use():
stats = MallocStats()
ctypes.CDLL(None).malloc_zone_statistics(None, ctypes.byref(stats))
return stats.size_in_use / (1024 * 1024)
NSObject = autoclass("NSObject")
NSMutableData = autoclass("NSMutableData")
print("retainCount of alloc().init():", NSObject.alloc().init().retainCount())
print("retainCount of new():", NSObject.new().retainCount())
before = malloc_mb_in_use()
for _ in range(100):
d = NSMutableData.alloc().initWithLength_(1024 * 1024)
del d
gc.collect()
print("still allocated after 100 x 1 MB alloc/init/del: {:.0f} MB".format(
malloc_mb_in_use() - before))
retainCount of alloc().init(): 2
retainCount of new(): 2
still allocated after 100 x 1 MB alloc/init/del: 125 MB
(macOS 26, arm64, master at 9fb0619.)
Cause
Every object a method returns is wrapped with instanciate_from(ret_id), which sends retain; the wrapper sends release once when it is collected:
That balances for objects returned at +0 (stringWithString:, dataWithLength:, …). Objects from the alloc, new, copy and mutableCopy families are returned already owned by the caller (+1), so the extra retain leaves them at 1 after the wrapper releases, and they are never deallocated.
Autoreleased objects
The +0 case is balanced, but those objects are still sitting in an autorelease pool, which only frees them when it is drained. Measured with the same malloc counter, 1 MB objects per iteration:
| no pool | inside a drained pool | |
|---|---|---|
NSMutableData.alloc().initWithLength_(1 MB) |
1280 KB | 1280 KB |
NSMutableData.dataWithLength_(1 MB) |
1280 KB | 0 KB |
(The pool was pushed and popped with objc_autoreleasePoolPush / objc_autoreleasePoolPop through ctypes.)
pyobjus has no way to do that itself. allocAndInitAutoreleasePool() and drainAutoreleasePool() are declared in _runtime.h and common.pxi but never called, and NSAutoreleasePool.alloc().init() aborts the process, because of the retain above:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSAutoreleasePool retain]: Cannot retain an autorelease pool'
So in scripts and tests, autoreleased objects pile up until the process (or thread) exits. I haven't checked how much of this a running Kivy app drains on its main thread.
Possible direction
- Don't add a
retainfor results of thealloc/new/copy/mutableCopyfamilies.selector_returns_retained()(added in #181 for delegate returns) already classifies these.init…needs care: it consumes the receiver's reference and returns a +1 object, which may not be the receiver (class clusters, ornil). - Expose an autorelease pool, e.g. a
with autoreleasepool():context manager overobjc_autoreleasePoolPush/objc_autoreleasePoolPop.
Rubicon has a good checklist of the cases a fix needs to get right, in tests/test_ObjCInstance.py: test_returned_lifecycle, test_alloc_lifecycle, test_alloc_init_lifecycle, test_new_lifecycle, test_copy_lifecycle, test_mutable_copy_lifecycle, test_immutable_copy_lifecycle (copy returns the same object) and test_init_change_lifecycle (init returns a different object). Each one checks that the object is deallocated once the Python wrapper is gone.
Related but separate: #176 (the return-value buffer leak).
- Dominant language
- Python
- Stars
- 192
- Forks
- 48
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 22
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from kivy/pyobjus
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 30/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 50/100
Maintainers usually reply within 1 day
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
BCDA-APS/polar-bits#93 ·
-
infer_audio_type only recognises one MPEG Layer III frame header, so most MP3 files are rejectedPossibly taken @Steve-20-ai claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
[BUG] - ssh_config `Match tagged` statement always appliesPossibly taken @AtharvRG claimed this today. OpenBug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
paramiko/paramiko#2715 · 1 comment ·
Maintainers usually reply within 9 days
-
auto-rejected intake mcp-intake needs-salvage priority:high type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Ikalus1988/MisakaNet#4221 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day