Skip to content

Conversation

@dpogue
Copy link
Member

@dpogue dpogue commented Sep 5, 2025

In older version of Cocoa with manual memory management, you'd create NSAutoreleasePool* objects in code and drain the pool at the end of the function/loop/whatever. You'd call [obj autorelease] to associate an object with a pool and it would automatically be destroyed when the pool was cleared.

In mac OS X 10.7, Apple introduced Automatic Reference Counting which forbids calling [obj autorelease] in Objective-C and also forbids creating NSAutoreleasePool objects manually. Instead, syntax sugar was introduced to the Clang compiler to automatically add autorelease pools using @autoreleasepool { } blocks. Internally, Clang turns these into calls to objc_autoreleasePoolPush() and objc_autoreleasePoolPop().

That left CoreFoundation objects (which are C API but also bridged with Objective-C) out of participating in autorelease pools, so in OS X 10.9 they introduced CFAutorelease() for CF objects.

To this day, you have the choice of compiling with Automatic Reference Counting or without Automatic Reference Counting for Objective-C objects, so we need to handle both cases.

This PR introduces:

  • bridge_cast<T>() to cast to Objective-C types (without changing the reference count if compiled with ARC)
  • hsAutorelease() to call either CFAutorelease() or [obj autorelease] (or nothing, in ARC mode) depending on the object type, with a polyfill for CFAutorelease() for older OS X versions
  • hsAutoreleasePool as a RAII type that will push an autorelease pool when it is created and drain the pool when it leaves scope
  • hsAutoreleasingScope as a quick way to create an anonymous hsAutoreleasePool for the duration of a block

It's somewhat weird to me that CFAutorelease was introduced, but there is no public documented API for creating autorelease pools in C. Supposedly there are _CFAutoreleasePoolPush() and _CFAutoreleasePoolPop() private functions that just call the Clang helpers directly, but they're also only available from OS X 10.7 onwards, so I opted to just use objc_msgSend to create an NSAutoreleasePool if we're on an older version or not using a compiler with the syntax sugar.

@dpogue dpogue requested a review from colincornaby September 5, 2025 05:13
@dpogue
Copy link
Member Author

dpogue commented Sep 5, 2025

One thing that I found confusing or non-obvious, is that this code crashes (but it also crashes if I use @autoreleasepool and [s autorelease] so it's not a bug in Plasma code)

{
    hsAutoreleasingScope;
    NSString* s = [NSString stringWithFormat:@"Hello %s", "world"];
    hsAutorelease(s);
}

I guess the NSString* returned via the Create Rule is already automatically added to the autorelease pool? It doesn't crash if I can [s retain] on it before calling hsAutorelease(s).

EDIT: Okay, I looked into this further and apparently [NSString stringWithFormat:] returns a pre-autoreleased string, whereas [[NSString alloc] initWithFormat:] would return a retained string, and everyone is just supposed to somehow know this intuitively.

Comment on lines 76 to 79
SEL autorelease = sel_registerName("autorelease");
IMP imp = class_getMethodImplementation(object_getClass(bridge_cast<id>(const_cast<void*>(ptr))), autorelease);
((CFTypeRef (*)(CFTypeRef, SEL))imp)(ptr, autorelease);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is jumping through several hoops to void the case where ARC tries to be overly helpful and causes double-free issues: https://www.mikeash.com/pyblog/friday-qa-2014-05-09-when-an-autorelease-isnt.html

@colincornaby
Copy link
Contributor

EDIT: Okay, I looked into this further and apparently [NSString stringWithFormat:] returns a pre-autoreleased string, whereas [[NSString alloc] initWithFormat:] would return a retained string, and everyone is just supposed to somehow know this intuitively.

The rule is if the function starts with init/new/copy (working from memory here) it's retained. If it doesn't, it's autoreleased.

@colincornaby
Copy link
Contributor

It's somewhat weird to me that CFAutorelease was introduced, but there is no public documented API for creating autorelease pools in C. Supposedly there are _CFAutoreleasePoolPush() and _CFAutoreleasePoolPop() private functions that just call the Clang helpers directly, but they're also only available from OS X 10.7 onwards, so I opted to just use objc_msgSend to create an NSAutoreleasePool if we're on an older version or not using a compiler with the syntax sugar.

Autorelease pools were not supposed to be part of CoreFoundation. CoreFoundation shipped on Mac OS 8 under Carbon - before Cocoa - so it's not 1:1. Autorelease pools were a uniquely Cocoa concept.

Casting to an NSObject was usually the best way this was handled. But pure CoreFoundation code should probably avoid Autorelease pools.

TEST(hsDarwin_Foundation, converts_to_ST_string)
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
hsAutoreleasingScope;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option with this scope type is we could just turn off ARC in places where the code needs to be shared. I'm not sure what your plan is with the client in since that targets a newer SDK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about the client, but I was thinking this would replace the hacking around pools I did in hsMessageBox

@dpogue
Copy link
Member Author

dpogue commented Sep 5, 2025

The rule is if the function starts with init/new/copy (working from memory here) it's retained. If it doesn't, it's autoreleased.

This is a bit outside the scope of this PR, but trying to wrap my head about this: Does our [NSString stringWithSTString:] class-level helper in plClient get this rule applied automatically by the compiler, or do we need to do something to mark it as returning an autoreleased string vs a retained string? Should that be wrapped in hsAutorelease so it's marked for autoreleasing even in non-ARC?

@colincornaby
Copy link
Contributor

The rule is if the function starts with init/new/copy (working from memory here) it's retained. If it doesn't, it's autoreleased.

This is a bit outside the scope of this PR, but trying to wrap my head about this: Does our [NSString stringWithSTString:] class-level helper in plClient get this rule applied automatically by the compiler, or do we need to do something to mark it as returning an autoreleased string vs a retained string? Should that be wrapped in hsAutorelease so it's marked for autoreleasing even in non-ARC?

No - ARC will automatically apply the autorelease. ARC reads the function names and automatically applies the correct ruleset. When calling ARC from ARC it doesn't really matter, the rule set application is only done so non-ARC code can call ARC code and get the matching behavior.

Autorelease should never be called directly on an Obj-C type in ARC. Hacking around that with CoreFoundation will lead to a bad time.

@colincornaby
Copy link
Contributor

I'm going to do a deeper review on this tonight, leaving a few comments in the meantime...

@dpogue
Copy link
Member Author

dpogue commented Sep 6, 2025

No - ARC will automatically apply the autorelease. ARC reads the function names and automatically applies the correct ruleset. When calling ARC from ARC it doesn't really matter, the rule set application is only done so non-ARC code can call ARC code and get the matching behavior.

What about non-ARC because it sounds like NSString stringWithFormat: and NSString stringWithSTString: will have different behaviour with one returning an autoreleased value and one returning a retained value.

With hsAutorelease on an Obj-C (id) type, in ARC it's a no-op, and in non-ARC it calls [obj autorelease]. That seems (to my naive understanding anyways) like what we want for NSString stringWithSTString:

@colincornaby
Copy link
Contributor

colincornaby commented Sep 6, 2025

What about non-ARC because it sounds like NSString stringWithFormat: and NSString stringWithSTString: will have different behaviour with one returning an autoreleased value and one returning a retained value.

Both should return an autoreleased reference in since neither function starts with the magic init/create/copy words.

For completeness I looked it up here:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html#//apple_ref/doc/uid/20000994-SW1

You own any object you create
You create an object using a method whose name begins with “alloc”, “new”, “copy”, or “mutableCopy” (for example, [alloc]).

Alloc is actually the init case. So the outer init is technically an autoreleased reference reference while the alloc is a retained reference. (init should retain on the way in and then autorelease on the way out.)

@dpogue
Copy link
Member Author

dpogue commented Sep 6, 2025

Both should return an autoreleased reference in since neither function starts with the magic init/create/copy words.

Okay, so in ARC this already worked because we return a CFBridgingRelease() to transfer ownership to ARC (when ARC is enabled). I've now added hsAutorelease() to the non-ARC case so that it should pick up the right behaviour.

Copy link
Contributor

@dgelessus dgelessus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @colincornaby that if we can completely avoid autoreleases in the non-Objective-C code, we should do that rather than introducing a custom compatibility/abstraction layer like this. (In that case, most of my comments below would become obsolete.)

But assuming that we do want this abstraction: the autoreleasepool code could be simplified a lot by implementing it in Objective-C. This would require making the functions not inline and moving the implementation out of the header, so that the header remains plain C++. (That may reduce performance, but if you're creating autoreleasepools in a tight loop, you're doing something wrong.)

+ (id)stringWithSTString:(const ST::string&)string
{
return NSStringCreateWithSTString(string);
return hsAutorelease(NSStringCreateWithSTString(string));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards this file just turning off ARC instead of adding hsAutorelease.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes look good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants