Reputation: 5710
Summary:
In the example application below, a shared_ptr
is being captured in an Objective-C block. The Objective-C block is being assigned to an ivar
of a dynamically created class using the Objective-C runtime API object_setIvarWithStrongDefault
. When the Objective-C object is deallocated, the shared_ptr
is leaking and the C++ object that it is retaining is not deleted. Why is that?
When object_setIvar
is used instead, then the leak is prevented but the ivar
points to garbage once the block goes out of scope as object_setIvar
assumes an assignment of unsafe_unretained
.
I assume this has to do with how Objective-C captures C++ objects, copies blocks and how shared_ptr
handles being copied, but I was hoping someone could shed some light on this more than the documentation listed below.
References:
Apple's Blocks and Variables Documentation contains a brief section on C++ objects but it's not entirely clear to me how it affects shared pointers.
LLVM's Documentation on Blocks & C++ Support is a bit more detailed than Apple's...
objc-class.mm contains the implementation for object_setIvarWithStrongDefault
Backstory:
This sample code is extracted from a much larger project and has been significantly reduced to the minimum required to show the issue. The project is an Objective-C macOS application. The application contains several monolithic C++ objects that are glorified key/value stores. Each object is an instance of the same class, but templated on the key type. I want to dynamically create an Objective-C class that contains typed property getters which are backed by the C++ class.
(Yes, this could all be done manually by just writing lots-and-lots of getters myself, but I'd prefer not to. The C++ class has enough information to know the names of the properties and their types, thus I'd like to use some meta-programming techniques to "solve" this.)
Notes:
In an ideal world, I'd just be able to define an iVar
on an Objective-C class of the appropriate shared_ptr
type but I can't figure out how to do that using the Objective-C runtime APIs.
Given this:
std::shared_ptr<BackingStore<T>> backingStore
How do you use this:
class_addIvar
and object_setIvar
Since I couldn't figure that out, I decided to just wrap the shared_ptr into an Objective-C block since blocks are first-class objects and can be passed around where an id
is expected.
Sample Application:
(Copy/paste into something like CodeRunner
to see output)
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <memory>
typedef NSString* (^stringBlock)();
/**
* StoreBridge
*
* Objective-C class that exposes Objective-C properties
* which are "backed" by a C++ object (Store). The implementations
* for each property on this class are dynamically added.
*/
@interface StoreBridge : NSObject
@property(nonatomic, strong, readonly) NSString *storeName;
@end
@implementation StoreBridge
@dynamic storeName;
- (void)dealloc {
NSLog(@"StoreBridge DEALLOC");
}
@end
/**
* BackingStore
*
* C++ class that for this example just exposes a single,
* hard-coded getter function. In reality this class is
* much larger.
*/
class BackingStore {
public:
BackingStore() {
NSLog(@"BackingStore constructor.");
}
~BackingStore() {
NSLog(@"BackingStore destructor.");
}
NSString *name() const {
return @"Amazon";
}
// Given a shared_ptr to a BackingStore instance, this method
// will dynamically create a new Objective-C class. The new
// class will contain Objective-C properties that are backed
// by the given BackingStore.
//
// Much of this code is hard-coded for this example. In reality,
// a much larger number of properties are dynamically created
// with different return types and a new class pair is
// only created if necessary.
static id makeBridge(std::shared_ptr<BackingStore> storePtr) {
// For this example, just create a new class pair each time.
NSString *klassName = NSUUID.UUID.UUIDString;
Class klass = objc_allocateClassPair(StoreBridge.class, klassName.UTF8String, 0);
// For this example, use hard-coded values and a single iVar definition. The
// iVar will store an Objective-C block as an 'id'.
size_t ivarSize = sizeof(id);
NSString *ivarName = @"_storeNameIvar";
NSString *encoding = [NSString stringWithFormat:@"%s@", @encode(id)];
SEL selector = @selector(storeName);
// Implementation for @property.storeName on StoreBridge. This
// implementation will read the block stored in the instances
// iVar named "_storeNameIvar" and call it. Fixed casting to
// type 'stringBlock' is used for this example only.
IMP implementation = imp_implementationWithBlock((id) ^id(id _self) {
Ivar iv = class_getInstanceVariable([_self class], ivarName.UTF8String);
id obj = object_getIvar(_self, iv);
return ((stringBlock)obj)();
});
// Add iVar definition and property implementation to newly created class pair.
class_addIvar(klass, ivarName.UTF8String, ivarSize, rint(log2(ivarSize)), @encode(id));
class_addMethod(klass, selector, implementation, encoding.UTF8String);
objc_registerClassPair(klass);
// Create instance of the newly defined class.
id bridge = [[klass alloc] init];
// Capture storePtr in an Objective-C block. This is the block that
// will be stored in the instance's iVar. Each bridge instance has
// its own backingStore, therefore the storePtr must be set on the
// instance's iVar and not captured in the implementation above.
id block = ^NSString* { return storePtr->name(); };
Ivar iva = class_getInstanceVariable(klass, ivarName.UTF8String);
// Assign block to previously declared iVar. When the strongDefault
// method is used, the shared_ptr will leak and the BackingStore
// will never get deallocated. When object_setIvar() is used,
// the BackingStore will get deallocated but crashes at
// runtime as 'block' is not retained anywhere.
//
// The documentation for object_setIvar() says that if 'strong'
// or 'weak' is not used, then 'unretained' is used. It might
// "work" in this example, but in a larger program it crashes
// as 'block' goes out of scope.
#define USE_STRONG_SETTER 1
#if USE_STRONG_SETTER
object_setIvarWithStrongDefault(bridge, iva, block);
#else
object_setIvar(bridge, iva, block);
#endif
return bridge;
}
};
int main(int argc, char *argv[]) {
@autoreleasepool {
std::shared_ptr<BackingStore> storePtr = std::make_shared<BackingStore>();
StoreBridge *bridge = BackingStore::makeBridge(storePtr);
NSLog(@"bridge.storeName: %@", bridge.storeName);
// When USE_STRONG_SETTER is 1, output is:
//
// > BackingStore constructor.
// > bridge.storeName: Amazon
// > StoreBridge DEALLOC
// When USE_STRONG_SETTER is 0, output is:
//
// > BackingStore constructor.
// > bridge.storeName: Amazon
// > BackingStore destructor.
// > StoreBridge DEALLOC
}
}
Upvotes: 3
Views: 1335
Reputation: 55583
Let's jump in a time machine real quick, C.A. 2010. It's a simpler time, before having to deal with multi-architecture slices, 64 bits, and other fancy things, like importantly ARC.
In this seemingly distant world to today, when you had memory, you had to release it yourself gasp. This meant, that if you had an iVar on your class, you had to explicitly, inside dealloc
call release
on it.
Well, this doesn't actually change with ARC. The only thing that changes is that the compiler generates all of those nice release
calls for you inside of dealloc
, even if you don't define the method. How nice.
The problem here, however, is that the compiler doesn't actually know about your iVar containing the block - it's completely defined at runtime. So how could the compiler release the memory?
The answer is it doesn't. You'll need to do some magic to make sure that you release this stuff at run-time. My suggestion would be to iterate over the iVars of the class, and set them to nil
, rather than call objc_release directly (as it causes much weeping and gnashing of teeth if you're using ARC).
Something like this:
for (ivar in class) {
if ivar_type == @encode(id) {
objc_setIvar(self, ivar, nil)
}
}
Now, if you ever go in and add an intentionally __unsafe_unretained ivar to this class you'll possibly have more issues. But you really shouldn't be inheriting from classes like this, mmkay?
Upvotes: 3