/*
* Copyright (C) 2012 RoboVM AB
*
* 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 org.robovm.objc;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.robovm.objc.annotation.NativeClass;
import org.robovm.objc.annotation.Property;
import org.robovm.rt.VM;
import org.robovm.rt.bro.NativeObject;
import org.robovm.rt.bro.Struct;
import org.robovm.rt.bro.annotation.Callback;
import org.robovm.rt.bro.annotation.Library;
import org.robovm.rt.bro.annotation.Marshaler;
import org.robovm.rt.bro.annotation.Marshalers;
import org.robovm.rt.bro.annotation.MarshalsPointer;
import org.robovm.rt.bro.annotation.Pointer;
import org.robovm.rt.bro.annotation.StructMember;
import org.robovm.rt.bro.ptr.Ptr;
import org.robovm.rt.bro.ptr.VoidPtr;
/**
*
*/
@Library("Foundation")
@NativeClass("Object")
@Marshalers({
@Marshaler(ObjCObject.Marshaler.class),
@Marshaler(ObjCClass.Marshaler.class)
})
public abstract class ObjCObject extends NativeObject {
private static volatile boolean logRetainRelease = false;
public static class ObjCObjectPtr extends Ptr<ObjCObject, ObjCObjectPtr> {}
static {
ObjCRuntime.bind(ObjCObject.class);
try {
Field f = ObjCObject.class.getDeclaredField("customClass");
CUSTOM_CLASS_OFFSET = VM.getInstanceFieldOffset(VM.getFieldAddress(f));
} catch (Throwable t) {
throw new Error(t);
}
NS_OBJECT_CLASS = ObjCRuntime.objc_getClass(VM.getStringUTFChars("NSObject"));
}
/**
* Common lock object used to prevent concurrent access to data in the Obj-C
* bridge (such as {@link ObjCObject#peers} and
* {@link ObjCClass#typeToClass}). This should be used to prevent deadlock
* situations from occurring. (#349)
*/
static final Object objcBridgeLock = new Object();
private static final LongMap<ObjCObjectRef> peers = new LongMap<>();
private static final long CUSTOM_CLASS_OFFSET;
private static final long NS_OBJECT_CLASS;
private ObjCSuper zuper;
protected final boolean customClass;
protected ObjCObject() {
long handle = alloc();
setHandle(handle);
if (handle != 0) {
// Make sure the peer is set immediately even if a different handle
// is set later with initObject().
setPeerObject(handle, this);
}
customClass = getObjCClass().isCustom();
}
protected ObjCObject(long handle) {
initObject(handle);
customClass = getObjCClass().isCustom();
}
ObjCObject(long handle, boolean customClass) {
initObject(handle);
this.customClass = customClass;
}
protected void initObject(long handle) {
if (handle == 0) {
throw new RuntimeException("Objective-C initialization method returned nil");
}
long oldHandle = getHandle();
if (handle != oldHandle) {
if (oldHandle != 0) {
removePeerObject(this);
}
setHandle(handle);
setPeerObject(handle, this);
}
}
protected long alloc() {
throw new UnsupportedOperationException("Cannot create instances of " + getClass().getName());
}
@Override
protected final void finalize() throws Throwable {
dispose(true);
}
public final void dispose() {
dispose(false);
}
protected void doDispose() {}
protected void dispose(boolean finalizing) {
long handle = getHandle();
if (handle != 0) {
removePeerObject(this);
doDispose();
setHandle(0);
}
if (finalizing) {
try {
super.finalize();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
@SuppressWarnings("unchecked")
protected ObjCSuper getSuper() {
if (zuper == null) {
Class<? extends ObjCObject> javaClass = (Class<? extends ObjCObject>) getClass().getSuperclass();
ObjCClass objCClass = ObjCClass.getByType(javaClass);
while (objCClass.isCustom()) {
javaClass = (Class<? extends ObjCObject>) javaClass.getSuperclass();
objCClass = ObjCClass.getByType(javaClass);
}
zuper = new ObjCSuper(this, objCClass);
}
return zuper;
}
protected void afterMarshaled(int flags) {}
public final ObjCClass getObjCClass() {
return ObjCClass.getFromObject(this);
}
@SuppressWarnings("unchecked")
static <T extends ObjCObject> T getPeerObject(long handle) {
synchronized (objcBridgeLock) {
ObjCObjectRef ref = peers.get(handle);
T o = ref != null ? (T) ref.get() : null;
return o;
}
}
private static void setPeerObject(long handle, ObjCObject o) {
synchronized (objcBridgeLock) {
if (o == null) {
peers.remove(handle);
} else {
peers.put(handle, new ObjCObjectRef(o));
}
}
}
private static void removePeerObject(ObjCObject o) {
synchronized (objcBridgeLock) {
long handle = o.getHandle();
ObjCObjectRef ref = peers.remove(handle);
ObjCObject p = ref != null ? ref.get() : null;
if (p != null && o != p) {
// Not the same peer. Put it back.
peers.put(handle, new ObjCObjectRef(p));
}
}
}
public <T extends Object> T addStrongRef(T to) {
AssociatedObjectHelper.addStrongRef(this, to);
return to;
}
public void removeStrongRef(Object to) {
AssociatedObjectHelper.removeStrongRef(this, to, false);
}
/**
* Updates a strong reference handling {@code null} values properly. This is
* meant to be used for {@link Property} setter methods with
* {@code strongRef=true}.
*
* @param before the previous value for the property. If not {@code null}
* and not equal to {@code after}
* {@link #removeStrongRef(Object)} will be called on this value.
* @param after the new value for the property. If not {@code null} and not
* equal to {@code after} {@link #addStrongRef(Object)} will be
* called on this value.
*/
public void updateStrongRef(Object before, Object after) {
if (before == after) {
// Either both are null or they reference the same object.
// If not null we assume that the property has already been set so
// that there already exists a strong reference.
return;
}
if (before != null) {
// Don't fail if the before value didn't have a strong reference.
// It could have been set from within ObjC.
AssociatedObjectHelper.removeStrongRef(this, before, true);
}
if (after != null) {
AssociatedObjectHelper.addStrongRef(this, after);
}
}
public Object getAssociatedObject(Object key) {
return AssociatedObjectHelper.getAssociatedObject(this, key);
}
public void setAssociatedObject(Object key, Object value) {
AssociatedObjectHelper.setAssociatedObject(this, key, value);
}
public static <T extends ObjCObject> T toObjCObject(Class<T> cls, long handle, int afterMarshaledFlags) {
return toObjCObject(cls, handle, afterMarshaledFlags, false);
}
@SuppressWarnings("unchecked")
public static <T extends ObjCObject> T toObjCObject(Class<T> cls, long handle, int afterMarshaledFlags, boolean forceType) {
if (handle == 0L) {
return null;
}
if (cls == ObjCClass.class) {
return (T) ObjCClass.toObjCClass(handle);
}
if (forceType) {
/*
* Always return a new instance without making it the new peer.
*/
return createInstance(ObjCClass.getByType(cls), handle, afterMarshaledFlags, false);
}
/*
* Determine the expected return type. Usually cls but it cls is an
* ObjCProxy class the expected type is instead the proxied interface.
*/
Class<?> expectedType = cls;
if (ObjCClass.isObjCProxy(cls)) {
expectedType = cls.getInterfaces()[0];
}
synchronized (objcBridgeLock) {
T o = getPeerObject(handle);
if (o != null && o.getHandle() != 0) {
if (!expectedType.isAssignableFrom(o.getClass())) {
if (ObjCClass.isObjCProxy(o.getClass())) {
/*
* The current peer is an incompatible ObjCProxy.
* Override that peer with a new one of the correct
* type.
*/
removePeerObject(o);
o = null;
} else if (ObjCClass.isObjCProxy(cls)) {
/*
* The current peer is not an ObjCProxy but we're
* expected to return one. Just return a new instance of
* the proxy without making it the peer.
*/
return createInstance(ObjCClass.getByType(cls), handle, afterMarshaledFlags, false);
} else {
/*
* Neither is an ObjCProxy. The current peer MUST be an
* instance of the expected type.
*/
throw new IllegalStateException("The peer object type " + o.getClass().getName()
+ " is not compatible with the expected type " + expectedType.getName());
}
} else {
return o;
}
}
ObjCClass objCClass = ObjCClass.getFromObject(handle);
if (!expectedType.isAssignableFrom(objCClass.getType())) {
/*
* If the expected return type is incompatible with the type of
* the native instance we have to make sure we return an
* instance of the expected type. See issue #821.
*/
objCClass = ObjCClass.getByType(cls);
}
return createInstance(objCClass, handle, afterMarshaledFlags, true);
}
}
/**
* Creates a new instance of the specified {@link ObjCClass}. If
* {@code makePeer == true} this method MUST be called while the
* {@link #objcBridgeLock} is held.
*/
@SuppressWarnings("unchecked")
private static <T extends ObjCObject> T createInstance(ObjCClass objCClass, long handle, int afterMarshaledFlags,
boolean makePeer) {
Class<T> c = (Class<T>) objCClass.getType();
T o = VM.allocateObject(c);
o.setHandle(handle);
if (makePeer) {
setPeerObject(handle, o);
}
if (objCClass.isCustom()) {
VM.setBoolean(VM.getObjectAddress(o) + CUSTOM_CLASS_OFFSET, true);
}
o.afterMarshaled(afterMarshaledFlags);
return o;
}
public static class Marshaler {
@MarshalsPointer
public static ObjCObject toObject(Class<? extends ObjCObject> cls, long handle, long flags) {
ObjCObject o = ObjCObject.toObjCObject(cls, handle, 0);
return o;
}
@MarshalsPointer
public static long toNative(ObjCObject o, long flags) {
if (o == null) {
return 0L;
}
return o.getHandle();
}
@MarshalsPointer
public static ObjCProtocol protocolToObject(Class<?> cls, long handle, long flags) {
Class<? extends ObjCObject> proxyClass = ObjCClass.allObjCProxyClasses.get(cls.getName());
if (proxyClass == null) {
proxyClass = ObjCObject.class;
}
ObjCObject o = ObjCObject.toObjCObject(proxyClass, handle, 0);
return (ObjCProtocol) o;
}
@MarshalsPointer
public static long protocolToNative(ObjCProtocol o, long flags) {
if (o == null) {
return 0L;
}
return ((ObjCObject) o).getHandle();
}
}
static class ObjCObjectRef extends WeakReference<ObjCObject> {
public final long handle;
public ObjCObjectRef(ObjCObject referent) {
super(referent);
handle = referent.getHandle();
}
}
static class ObjectOwnershipHelper {
private static final LongMap<Object> CUSTOM_OBJECTS = new LongMap<>();
private static final long retainCount = Selector.register("retainCount").getHandle();
private static final long retain = Selector.register("retain").getHandle();
private static final long originalRetain = Selector.register("original_retain").getHandle();
private static final long release = Selector.register("release").getHandle();
private static final long originalRelease = Selector.register("original_release").getHandle();
private static final Method retainMethod;
private static final Method releaseMethod;
private static final LongMap<Long> customClassToNativeSuper = new LongMap<>();
private static final Long ZERO_LONG = Long.valueOf(0);
static {
try {
retainMethod = ObjectOwnershipHelper.class.getDeclaredMethod("retain", Long.TYPE, Long.TYPE);
releaseMethod = ObjectOwnershipHelper.class.getDeclaredMethod("release", Long.TYPE, Long.TYPE);
} catch (Throwable t) {
throw new Error(t);
}
}
public static void registerClass(long cls) {
registerCallbackMethod(cls, retain, originalRetain, retainMethod);
registerCallbackMethod(cls, release, originalRelease, releaseMethod);
}
private static void registerCallbackMethod(long cls, long selector, long newSelector, Method method) {
long superMethod = ObjCRuntime.class_getInstanceMethod(cls, selector);
long typeEncoding = ObjCRuntime.method_getTypeEncoding(superMethod);
if (!ObjCRuntime.class_addMethod(cls, selector, VM.getCallbackMethodImpl(method), typeEncoding)) {
throw new Error(
"Failed to register callback method on the ObjectOwnershipHelper: class_addMethod(...) failed");
}
// find the super class that is a native class and cache it
long superClass = ObjCRuntime.class_getSuperclass(cls);
long nativeSuper = 0;
while (superClass != 0) {
ObjCClass objCClass = ObjCClass.toObjCClass(superClass);
if (!objCClass.isCustom()) {
nativeSuper = superClass;
break;
}
superClass = ObjCRuntime.class_getSuperclass(superClass);
}
if (nativeSuper == 0) {
throw new Error("Couldn't find native super class for "
+ VM.newStringUTF(ObjCRuntime.class_getName(cls)));
}
synchronized (customClassToNativeSuper) {
customClassToNativeSuper.put(cls, nativeSuper);
}
}
@Callback
private static @Pointer long retain(@Pointer long self, @Pointer long sel) {
int count = ObjCRuntime.int_objc_msgSend(self, retainCount);
if (count <= 1) {
synchronized (CUSTOM_OBJECTS) {
ObjCClass cls = ObjCClass.toObjCClass(ObjCRuntime.object_getClass(self));
ObjCObject obj = ObjCObject.toObjCObject(cls.getType(), self, 0);
CUSTOM_OBJECTS.put(self, obj);
}
}
long cls = ObjCRuntime.object_getClass(self);
if (logRetainRelease) {
logRetainRelease(cls, self, count, true);
}
Super sup = new Super(self, getNativeSuper(cls));
return ObjCRuntime.ptr_objc_msgSendSuper(sup.getHandle(), sel);
}
@Callback
private static void release(@Pointer long self, @Pointer long sel) {
int count = ObjCRuntime.int_objc_msgSend(self, retainCount);
if (count <= 2) {
synchronized (CUSTOM_OBJECTS) {
CUSTOM_OBJECTS.remove(self);
}
}
long cls = ObjCRuntime.object_getClass(self);
if (logRetainRelease) {
logRetainRelease(cls, self, count, false);
}
Super sup = new Super(self, getNativeSuper(cls));
ObjCRuntime.void_objc_msgSendSuper(sup.getHandle(), sel);
}
public static boolean isObjectRetained(ObjCObject object) {
synchronized (CUSTOM_OBJECTS) {
return CUSTOM_OBJECTS.containsKey(object.getHandle());
}
}
private static long getNativeSuper(final long cls) {
/*
* We cannot just assume that cls is a custom class that has an
* entry in customClassToNativeSuper. The Obj-C runtime will
* sometimes subclass our custom classes (e.g. when doing key-value
* observing) which means that retain()/release() in this class may
* be called with instances of such subclasses. We must walk the
* class hierarchy to find the actual custom class.
*/
long c = cls;
synchronized (customClassToNativeSuper) {
while (c != 0) {
long nativeSuper = customClassToNativeSuper.get(c, ZERO_LONG);
if (nativeSuper != 0) {
return nativeSuper;
}
c = ObjCRuntime.class_getSuperclass(c);
}
}
List<String> classHierarchy = new ArrayList<>();
c = cls;
while (c != 0) {
classHierarchy.add(VM.newStringUTF(ObjCRuntime.class_getName(c)));
c = ObjCRuntime.class_getSuperclass(c);
}
throw new Error("Failed to find a custom class to native super class "
+ "mapping for class hierarchy " + classHierarchy);
}
}
static class AssociatedObjectHelper {
private static final String STRONG_REFS_KEY = AssociatedObjectHelper.class.getName() + ".StrongRefs";
private static final int OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1;
private static final long RELEASE_LISTENER_CLASS;
private static final String OWNER_IVAR_NAME = "value";
private static final int OWNER_IVAR_OFFSET;
private static final Selector alloc = Selector.register("alloc");
private static final Selector init = Selector.register("init");
private static final Selector release = Selector.register("release");
private static final Selector retainCount = Selector.register("retainCount");
private static final LongMap<Map<Object, Object>> ASSOCIATED_OBJECTS = new LongMap<>();
static {
int ptrSize = VoidPtr.sizeOf();
int alignment = ptrSize == 4 ? 2 : 3;
long cls = ObjCRuntime.objc_allocateClassPair(NS_OBJECT_CLASS,
VM.getStringUTFChars("RoboVMReleaseListener"), ptrSize);
if (cls == 0L) {
throw new Error(
"Failed to create the RoboVMReleaseListener Objective-C class: objc_allocateClassPair(...) failed");
}
if (!ObjCRuntime.class_addIvar(cls, VM.getStringUTFChars(OWNER_IVAR_NAME), ptrSize, (byte) alignment,
VM.getStringUTFChars("?"))) {
throw new Error(
"Failed to create the RoboVMAssocObjWrapper Objective-C class: class_addIvar(...) failed");
}
Method releaseMethod = null;
try {
releaseMethod = AssociatedObjectHelper.class.getDeclaredMethod("release", Long.TYPE, Long.TYPE);
} catch (Throwable t) {
throw new Error(t);
}
long superReleaseMethod = ObjCRuntime.class_getInstanceMethod(NS_OBJECT_CLASS, release.getHandle());
long releaseType = ObjCRuntime.method_getTypeEncoding(superReleaseMethod);
if (!ObjCRuntime.class_addMethod(cls, release.getHandle(), VM.getCallbackMethodImpl(releaseMethod),
releaseType)) {
throw new Error(
"Failed to create the RoboVMReleaseListener Objective-C class: class_addMethod(...) failed");
}
ObjCRuntime.objc_registerClassPair(cls);
RELEASE_LISTENER_CLASS = cls;
OWNER_IVAR_OFFSET = ObjCRuntime.ivar_getOffset(ObjCRuntime.class_getInstanceVariable(cls,
VM.getStringUTFChars(OWNER_IVAR_NAME)));
}
private static void enableListener(long handle) {
long releaseListener = ObjCRuntime.objc_getAssociatedObject(handle, RELEASE_LISTENER_CLASS);
if (releaseListener == 0) {
releaseListener = ObjCRuntime.ptr_objc_msgSend(RELEASE_LISTENER_CLASS, alloc.getHandle());
if (releaseListener == 0L) {
throw new OutOfMemoryError();
}
releaseListener = ObjCRuntime.ptr_objc_msgSend(releaseListener, init.getHandle());
VM.setPointer(releaseListener + OWNER_IVAR_OFFSET, handle);
ObjCRuntime.objc_setAssociatedObject(handle, RELEASE_LISTENER_CLASS, releaseListener,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
ObjCRuntime.void_objc_msgSend(releaseListener, release.getHandle());
}
}
private static void disableListener(long handle) {
ObjCRuntime.objc_setAssociatedObject(handle, RELEASE_LISTENER_CLASS, 0L, 0);
}
public static Object getAssociatedObject(ObjCObject object, Object key) {
synchronized (ASSOCIATED_OBJECTS) {
Map<Object, Object> map = ASSOCIATED_OBJECTS.get(object.getHandle());
if (map == null) {
return null;
}
return map.get(key);
}
}
public static void setAssociatedObject(ObjCObject object, Object key, Object value) {
synchronized (ASSOCIATED_OBJECTS) {
Map<Object, Object> map = ASSOCIATED_OBJECTS.get(object.getHandle());
if (map == null && value == null) {
return;
}
if (map == null) {
map = new HashMap<Object, Object>();
enableListener(object.getHandle());
ASSOCIATED_OBJECTS.put(object.getHandle(), map);
}
if (value != null) {
map.put(key, value);
} else {
map.remove(key);
if (map.isEmpty()) {
disableListener(object.getHandle());
ASSOCIATED_OBJECTS.remove(object.getHandle());
}
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void addStrongRef(ObjCObject from, Object to) {
if (to == null) {
throw new NullPointerException();
}
synchronized (ASSOCIATED_OBJECTS) {
List l = (List) getAssociatedObject(from, STRONG_REFS_KEY);
if (l == null) {
l = new ArrayList();
setAssociatedObject(from, STRONG_REFS_KEY, l);
}
l.add(to);
}
}
@SuppressWarnings("rawtypes")
public static void removeStrongRef(ObjCObject from, Object to, boolean ignoreNotExists) {
if (to == null) {
throw new NullPointerException();
}
synchronized (ASSOCIATED_OBJECTS) {
List l = (List) getAssociatedObject(from, STRONG_REFS_KEY);
if (!ignoreNotExists && (l == null || !l.remove(to))) {
throw new IllegalArgumentException("No strong ref exists from " + from
+ " (a " + from.getClass().getName() + ") to " + to
+ " a (" + to.getClass().getName() + ")");
}
if (l != null && l.isEmpty()) {
setAssociatedObject(from, STRONG_REFS_KEY, null);
}
}
}
@Callback
static void release(@Pointer long self, @Pointer long sel) {
int count = ObjCRuntime.int_objc_msgSend(self, retainCount.getHandle());
if (count == 1) {
long owner = VM.getPointer(self + OWNER_IVAR_OFFSET);
synchronized (ASSOCIATED_OBJECTS) {
ASSOCIATED_OBJECTS.remove(owner);
}
}
if(logRetainRelease) {
long cls = ObjCRuntime.object_getClass(self);
logRetainRelease(cls, self, count, false);
}
ObjCRuntime.void_objc_msgSendSuper(new Super(self, NS_OBJECT_CLASS).getHandle(), sel);
}
}
public static final class Super extends Struct<Super> {
public Super(long receiver, long objcClass) {
receiver(receiver);
objCClass(objcClass);
}
@StructMember(0)
public native @Pointer long receiver();
@StructMember(0)
public native Super receiver(@Pointer long receiver);
@StructMember(1)
public native @Pointer long objCClass();
@StructMember(1)
public native Super objCClass(@Pointer long objCClass);
}
/**
* Sets whether retain/release of custom {@link ObjCObject} should be logged
* to the console to identify retain/release leaks. Note that the GC has to
* be able to collect the custom object for the final release to be
* triggered.</p>
*
* The output logs the class, memory address and retain count after the
* release/retain invocation. You can use the memory address to inspect
* custom objects in Instruments.
*/
public static void logRetainRelease(boolean enabled) {
logRetainRelease = enabled;
}
private static void logRetainRelease(long cls, long self, int count, boolean isRetain) {
String className = ObjCClass.getFromObject(cls).getType().getName();
System.err.println(String.format("[Debug] %s %s@0x%s, retain count: %d",
isRetain ? "Retained" : "Released", className, Long.toHexString(self), isRetain ? count + 1
: count - 1));
}
}