All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.bugvm.objc.ObjCObject Maven / Gradle / Ivy

The newest version!
/*
 * 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 com.bugvm.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 com.bugvm.objc.annotation.NativeClass;
import com.bugvm.objc.annotation.Property;
import com.bugvm.rt.VM;
import com.bugvm.rt.bro.NativeObject;
import com.bugvm.rt.bro.Struct;
import com.bugvm.rt.bro.annotation.Callback;
import com.bugvm.rt.bro.annotation.Library;
import com.bugvm.rt.bro.annotation.Marshaler;
import com.bugvm.rt.bro.annotation.Marshalers;
import com.bugvm.rt.bro.annotation.MarshalsPointer;
import com.bugvm.rt.bro.annotation.Pointer;
import com.bugvm.rt.bro.annotation.StructMember;
import com.bugvm.rt.bro.ptr.Ptr;
import com.bugvm.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 {}

    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 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 javaClass = (Class) getClass().getSuperclass();
            ObjCClass objCClass = ObjCClass.getByType(javaClass);
            while (objCClass.isCustom()) {
                javaClass = (Class) 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 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 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 toObjCObject(Class cls, long handle, int afterMarshaledFlags) {
        return toObjCObject(cls, handle, afterMarshaledFlags, false);
    }

    @SuppressWarnings("unchecked")
    public static  T toObjCObject(Class 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 createInstance(ObjCClass objCClass, long handle, int afterMarshaledFlags,
            boolean makePeer) {

        Class c = (Class) 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 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 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 {
        public final long handle;

        public ObjCObjectRef(ObjCObject referent) {
            super(referent);
            handle = referent.getHandle();
        }
    }

    static class ObjectOwnershipHelper {
        private static final LongMap 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 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 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> 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("BugVMReleaseListener"), ptrSize);
            if (cls == 0L) {
                throw new Error(
                        "Failed to create the BugVMReleaseListener 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 BugVMAssocObjWrapper 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 BugVMReleaseListener 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 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 map = ASSOCIATED_OBJECTS.get(object.getHandle());
                if (map == null && value == null) {
                    return;
                }
                if (map == null) {
                    map = new HashMap();
                    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 {

        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.

* * 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)); } }