Skip to content

Commit

Permalink
Adds @RetainedWith, which can be used to prevent leaks from certain r…
Browse files Browse the repository at this point in the history
…eference cycles.

	Change on 2016/05/25 by kstanger <kstanger@google.com>

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=123238011
  • Loading branch information
kstanger authored and tomball committed May 27, 2016
1 parent 8294517 commit 44a70d0
Show file tree
Hide file tree
Showing 14 changed files with 495 additions and 18 deletions.
1 change: 1 addition & 0 deletions annotations/classes.mk
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ ANNOTATION_SOURCE_JAVA = \
com/google/j2objc/annotations/Property.java \
com/google/j2objc/annotations/ReflectionSupport.java \
com/google/j2objc/annotations/RetainedLocalRef.java \
com/google/j2objc/annotations/RetainedWith.java \
com/google/j2objc/annotations/Weak.java \
com/google/j2objc/annotations/WeakOuter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.j2objc.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* PLEASE READ THIS DOCUMENTATION BEFORE USING THIS ANNOTATION!
* Note the criteria listed below which cannot be enforced by static analysis in
* j2objc. Prefer using {@link Weak} over @RetainedWith if possible.
*
* Indicates that the annotated field (child) forms a direct reference cycle
* with the referring object (parent). Adding this annotation informs J2ObjC of
* the reference cycle between the pair of objects allowing both objects to be
* deallocated once there are no external references to either object.
*
* @RetainedWith can be applied when a parent object pairs itself with a child
* object and cannot fully manage the child's lifecycle, usually because the
* child is returned to the caller. It can also be applied when the child has
* the same class as the parent, for example on an inverse field of an
* invertible collection type.
*
* @RetainedWith can only be applied where there is a two-object pair with a
* cycle created by one reference from each object. Note: the two references can
* be from the same declared field. When the references are from different
* fields only one field should be given a @RetainedWith annotation, and the
* @RetainedWith field should point from parent to child.
*
* When applied carefully in the right circumstance this annotation is very
* useful in preventing leaks from certain Java collection types without needing
* to alter their behavior.
*
* The following criteria must be adhered to otherwise behavior will be
* undefined:
* - The @RetainedWithfield, once assigned, must not be reassigned.
* - The child object must not reassign any references back to the parent
* object. Preferably references from child to parent are declared final.
* - The child object must not contain any {@link Weak} references back to the
* parent object.
* - The child object must not be available to other threads at the time of
* assignment. (Normally, the cycle is created upon construction of the child)
*
* @author Keith Stanger
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.CLASS)
public @interface RetainedWith {
}
5 changes: 5 additions & 0 deletions jre_emul/Classes/J2ObjC_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ void JreCloneVolatile(volatile_id *pVar, volatile_id *pOther);
void JreCloneVolatileStrong(volatile_id *pVar, volatile_id *pOther);
void JreReleaseVolatile(volatile_id *pVar);

id JreRetainedWithAssign(id parent, __strong id *pIvar, id value);
id JreVolatileRetainedWithAssign(id parent, volatile_id *pIvar, id value);
void JreRetainedWithRelease(id parent, id child);
void JreVolatileRetainedWithRelease(id parent, volatile_id *pVar);

NSString *JreStrcat(const char *types, ...);

#if defined(J2OBJC_COUNT_NIL_CHK) && !defined(J2OBJC_DISABLE_NIL_CHECKS)
Expand Down
41 changes: 41 additions & 0 deletions jre_emul/Classes/J2ObjC_common.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#import "FastPointerLookup.h"
#import "IOSClass.h"
#import "JreRetainedWith.h"
#import "java/lang/AbstractStringBuilder.h"
#import "java/lang/AssertionError.h"
#import "java/lang/ClassCastException.h"
Expand Down Expand Up @@ -182,6 +183,46 @@ void JreCloneVolatileStrong(volatile_id *pVar, volatile_id *pOther) {
VOLATILE_UNLOCK(lock);
}

id JreRetainedWithAssign(id parent, __strong id *pIvar, id value) {
if (*pIvar) {
@throw create_JavaLangAssertionError_initWithId_(@"@RetainedWith field cannot be reassigned");
}
// This retain makes sure that the child object has a retain count of at
// least 2 which is required by JreRetainedWithInitialize.
[value retain];
JreRetainedWithInitialize(parent, value);
return *pIvar = value;
}

id JreVolatileRetainedWithAssign(id parent, volatile_id *pIvar, id value) {
// This retain makes sure that the child object has a retain count of at
// least 2 which is required by JreRetainedWithInitialize.
[value retain];
JreRetainedWithInitialize(parent, value);
volatile_lock_t *lock = VOLATILE_GETLOCK(pIvar);
VOLATILE_LOCK(lock);
id oldValue = *(id *)pIvar;
*(id *)pIvar = value;
VOLATILE_UNLOCK(lock);
if (oldValue) {
@throw create_JavaLangAssertionError_initWithId_(@"@RetainedWith field cannot be reassigned");
}
return value;
}

void JreRetainedWithRelease(id parent, id value) {
JreRetainedWithHandleDealloc(parent, value);
[value release];
}

void JreVolatileRetainedWithRelease(id parent, volatile_id *pVar) {
JreRetainedWithHandleDealloc(parent, *(id *)pVar);
// This is only called from a dealloc method, so we can assume there are no
// concurrent threads with access to this address. Therefore, synchronization
// is unnecessary.
[*(id *)pVar release];
}

// Block flag position for copy dispose, (1 << 25).
#define COPY_DISPOSE_FLAG 0x02000000

Expand Down
33 changes: 33 additions & 0 deletions jre_emul/Classes/JreRetainedWith.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//
// JreRetainedWith.h
// JreEmulation
//
// Created by Keith Stanger on Mar. 18, 2016.
//
// INTERNAL ONLY. For use by JRE emulation code.

#ifndef JRE_RETAINED_WITH_H_
#define JRE_RETAINED_WITH_H_

#import <Foundation/Foundation.h>

// Called by @RetainedWith assignment functions. Caller must ensure that value
// has a retain count at least two.
FOUNDATION_EXPORT void JreRetainedWithInitialize(id parent, id value);

// Called during dealloc of the parent and before releasing the child.
FOUNDATION_EXPORT void JreRetainedWithHandleDealloc(id parent, id child);

#endif // JRE_RETAINED_WITH_H_
143 changes: 143 additions & 0 deletions jre_emul/Classes/JreRetainedWith.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//
// JreRetainedWith.h
// JreEmulation
//
// Created by Keith Stanger on Mar. 18, 2016.
//

#include "FastPointerLookup.h"
#include "J2ObjC_source.h"

// Associate the return reference so that it can be artificially weakened when
// the child's retain count is 1.
static char returnRefKey;
// Associate the child's original class for "super" calls in the swizzled
// methods.
static char superClsKey;

static id RetainedWithRetain(id self, SEL _cmd) {
@synchronized (self) {
if ([self retainCount] == 1) {
[objc_getAssociatedObject(self, &returnRefKey) retain];
}
Class superCls = objc_getAssociatedObject(self, &superClsKey);
IMP superRetain = class_getMethodImplementation(superCls, @selector(retain));
return ((id (*)(id, SEL))superRetain)(self, @selector(retain));
}
}

static void RetainedWithRelease(id self, SEL _cmd) {
@synchronized (self) {
if ([self retainCount] == 2) {
[objc_getAssociatedObject(self, &returnRefKey) autorelease];
}
Class superCls = objc_getAssociatedObject(self, &superClsKey);
IMP superRelease = class_getMethodImplementation(superCls, @selector(release));
((id (*)(id, SEL))superRelease)(self, @selector(release));
}
}

static IOSClass *RetainedWithGetClass(id self, SEL _cmd) {
Class superCls = objc_getAssociatedObject(self, &superClsKey);
return IOSClass_fromClass(superCls);
}

// Creates a subclass with swizzled retain, release, and getClass methods.
static void *CreateSubclass(void *clsPtr) {
Class cls = (Class)clsPtr;
NSString *newName = [NSString stringWithFormat:@"%s_JreRetainedWith", class_getName(cls)];
Class subclass = objc_allocateClassPair(cls, [newName UTF8String], 0);
Method retain = class_getInstanceMethod(cls, @selector(retain));
class_addMethod(subclass, @selector(retain), (IMP)RetainedWithRetain,
method_getTypeEncoding(retain));
Method release = class_getInstanceMethod(cls, @selector(release));
class_addMethod(subclass, @selector(release), (IMP)RetainedWithRelease,
method_getTypeEncoding(release));
Method getClass = class_getInstanceMethod(cls, @selector(getClass));
class_addMethod(subclass, @selector(getClass), (IMP)RetainedWithGetClass,
method_getTypeEncoding(getClass));
objc_registerClassPair(subclass);
return subclass;
}

static FastPointerLookup_t subclassLookup = FAST_POINTER_LOOKUP_INIT(&CreateSubclass);

// Swizzle the class of the child and make necessary associations.
static void ApplyRetainedWithSubclass(id parent, id child) {
Class cls = object_getClass(child);
Class subclass = (Class)FastPointerLookup(&subclassLookup, cls);
objc_setAssociatedObject(child, &returnRefKey, parent, OBJC_ASSOCIATION_ASSIGN);
objc_setAssociatedObject(child, &superClsKey, cls, OBJC_ASSOCIATION_ASSIGN);
object_setClass(child, subclass);
}

// Counts how many times the child refers back to the parent.
static NSUInteger CountReturnRefs(id parent, id child) {
NSUInteger returnRefs = 0;
Class cls = object_getClass(child);
while (cls && cls != [NSObject class]) {
unsigned int ivarCount;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
for (unsigned int i = 0; i < ivarCount; i++) {
Ivar ivar = ivars[i];
const char *ivarType = ivar_getTypeEncoding(ivar);
if (*ivarType == '@') {
ptrdiff_t offset = ivar_getOffset(ivar);
if (*(id *)((uintptr_t)child + offset) == parent) {
returnRefs++;
};
}
}
free(ivars);
cls = class_getSuperclass(cls);
}
return returnRefs;
}

// Called upon destruction of the parent. We must set all return refs to nil to
// avoid calling release on a dealloc'ed object from child's dealloc method.
void JreRetainedWithHandleDealloc(id parent, id child) {
Class cls = object_getClass(child);
while (cls && cls != [NSObject class]) {
unsigned int ivarCount;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
for (unsigned int i = 0; i < ivarCount; i++) {
Ivar ivar = ivars[i];
const char *ivarType = ivar_getTypeEncoding(ivar);
if (*ivarType == '@') {
ptrdiff_t offset = ivar_getOffset(ivar);
id *objField = (id *)((uintptr_t)child + offset);
if (*objField == parent) {
*objField = nil;
};
}
}
free(ivars);
cls = class_getSuperclass(cls);
}
}

// Requires that value has a retain count of at least 2 so that its return
// reference can remain strong to start.
void JreRetainedWithInitialize(id parent, id value) {
NSUInteger returnRefs = CountReturnRefs(parent, value);
if (returnRefs > 0) {
// Make all but one of the return refs weak.
while (returnRefs-- > 1) {
[parent release];
}
ApplyRetainedWithSubclass(parent, value);
}
}
1 change: 1 addition & 0 deletions jre_emul/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ IOS_PUBLIC_OBJS = \
IOSPrimitiveArray.o \
IOSReflection.o \
J2ObjC_common.o \
JreRetainedWith.o \
NSCopying+JavaCloneable.o \
NSDataInputStream.o \
NSDataOutputStream.o \
Expand Down
Loading

0 comments on commit 44a70d0

Please sign in to comment.