Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Objects created with alloc/init, new or mutableCopy are never freed

Open
#194 0 comments 0 reactions 0 assignees View on GitHub

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:

https://github.com/kivy/pyobjus/blob/9fb061997d78d5156a24419a0cfd6416f3b3ec5b/pyobjus/pyobjus_conversions.pxi#L187

https://github.com/kivy/pyobjus/blob/9fb061997d78d5156a24419a0cfd6416f3b3ec5b/pyobjus/pyobjus_types.pxi#L715-L724

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 retain for results of the alloc / new / copy / mutableCopy families. 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, or nil).
  • Expose an autorelease pool, e.g. a with autoreleasepool(): context manager over objc_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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from kivy/pyobjus

All issues in kivy/pyobjus

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.