Objective-C API: JSObjCClassInfo creates reference cycle with JSContext
[WebKit-https.git] / Source / JavaScriptCore / API / JSWrapperMap.mm
index ce3ec7e..a268a56 100644 (file)
 
 #if JS_OBJC_API_ENABLED
 
+#import "APICast.h"
 #import "JSContextInternal.h"
 #import "JSWrapperMap.h"
 #import "ObjCCallbackFunction.h"
 #import "ObjcRuntimeExtras.h"
+#import "Operations.h"
+#import "WeakGCMap.h"
 #import <wtf/TCSpinLock.h>
 #import "wtf/Vector.h"
 
+@class JSObjCClassInfo;
+
+@interface JSWrapperMap () 
+
+- (JSObjCClassInfo*)classInfoForClass:(Class)cls;
+
+@end
+
 static void wrapperFinalize(JSObjectRef object)
 {
     [(id)JSObjectGetPrivate(object) release];
@@ -61,7 +72,7 @@ static JSClassRef wrapperClass()
 
 // Default conversion of selectors to property names.
 // All semicolons are removed, lowercase letters following a semicolon are capitalized.
-static NSStringselectorToPropertyName(const char* start)
+static NSString *selectorToPropertyName(const char* start)
 {
     // Use 'index' to check for colons, if there are non, this is easy!
     const char* firstColon = index(start, ':');
@@ -72,7 +83,7 @@ static NSString* selectorToPropertyName(const char* start)
     size_t header = firstColon - start;
     // The new string needs to be long enough to hold 'header', plus the remainder of the string, excluding
     // at least one ':', but including a '\0'. (This is conservative if there are more than one ':').
-    char* buffer = (char*)malloc(header + strlen(firstColon + 1) + 1);
+    char* buffer = static_cast<char*>(malloc(header + strlen(firstColon + 1) + 1));
     // Copy 'header' characters, set output to point to the end of this & input to point past the first ':'.
     memcpy(buffer, start, header);
     char* output = buffer + header;
@@ -98,7 +109,7 @@ static NSString* selectorToPropertyName(const char* start)
         // If we get here, we've consumed a ':' - wash, rinse, repeat.
     }
 done:
-    NSStringresult = [NSString stringWithUTF8String:buffer];
+    NSString *result = [NSString stringWithUTF8String:buffer];
     free(buffer);
     return result;
 }
@@ -106,7 +117,7 @@ done:
 // Make an object that is in all ways are completely vanilla JavaScript object,
 // other than that it has a native brand set that will be displayed by the default
 // Object.prototype.toString comversion.
-static JSValue* createObjectWithCustomBrand(JSContext* context, NSString* brand, JSClassRef parentClass = 0, void* privateData = 0)
+static JSValue *createObjectWithCustomBrand(JSContext *context, NSString *brand, JSClassRef parentClass = 0, void* privateData = 0)
 {
     JSClassDefinition definition;
     definition = kJSClassDefinitionEmpty;
@@ -120,26 +131,26 @@ static JSValue* createObjectWithCustomBrand(JSContext* context, NSString* brand,
 
 // Look for @optional properties in the prototype containing a selector to property
 // name mapping, separated by a __JS_EXPORT_AS__ delimiter.
-static NSMutableDictionary* createRenameMap(Protocol* protocol, BOOL isInstanceMethod)
+static NSMutableDictionary *createRenameMap(Protocol *protocol, BOOL isInstanceMethod)
 {
-    NSMutableDictionaryrenameMap = [[NSMutableDictionary alloc] init];
+    NSMutableDictionary *renameMap = [[NSMutableDictionary alloc] init];
 
     forEachMethodInProtocol(protocol, NO, isInstanceMethod, ^(SEL sel, const char*){
-        NSStringrename = @(sel_getName(sel));
+        NSString *rename = @(sel_getName(sel));
         NSRange range = [rename rangeOfString:@"__JS_EXPORT_AS__"];
         if (range.location == NSNotFound)
             return;
-        NSStringselector = [rename substringToIndex:range.location];
+        NSString *selector = [rename substringToIndex:range.location];
         NSUInteger begin = range.location + range.length;
         NSUInteger length = [rename length] - begin - 1;
-        NSStringname = [rename substringWithRange:(NSRange){ begin, length }];
+        NSString *name = [rename substringWithRange:(NSRange){ begin, length }];
         renameMap[selector] = name;
     });
 
     return renameMap;
 }
 
-inline void putNonEnumerable(JSValue* base, NSString* propertyName, JSValue* value)
+inline void putNonEnumerable(JSValue *base, NSString *propertyName, JSValue *value)
 {
     [base defineProperty:propertyName descriptor:@{
         JSPropertyDescriptorValueKey: value,
@@ -153,13 +164,13 @@ inline void putNonEnumerable(JSValue* base, NSString* propertyName, JSValue* val
 //  * Determine a property name (either via a renameMap or default conversion).
 //  * If an accessorMap is provided, and conatins a this name, store the method in the map.
 //  * Otherwise, if the object doesn't already conatin a property with name, create it.
-static void copyMethodsToObject(JSContext* context, Class objcClass, Protocol* protocol, BOOL isInstanceMethod, JSValue* object, NSMutableDictionary* accessorMethods = nil)
+static void copyMethodsToObject(JSContext *context, Class objcClass, Protocol *protocol, BOOL isInstanceMethod, JSValue *object, NSMutableDictionary *accessorMethods = nil)
 {
-    NSMutableDictionaryrenameMap = createRenameMap(protocol, isInstanceMethod);
+    NSMutableDictionary *renameMap = createRenameMap(protocol, isInstanceMethod);
 
     forEachMethodInProtocol(protocol, YES, isInstanceMethod, ^(SEL sel, const char* types){
         const char* nameCStr = sel_getName(sel);
-        NSStringname = @(nameCStr);
+        NSString *name = @(nameCStr);
         if (accessorMethods && accessorMethods[name]) {
             JSObjectRef method = objCCallbackFunctionForMethod(context, objcClass, protocol, isInstanceMethod, sel, types);
             if (!method)
@@ -220,7 +231,7 @@ static char* makeSetterName(const char* name)
     return setterName;
 }
 
-static void copyPrototypeProperties(JSContext* context, Class objcClass, Protocol* protocol, JSValue* prototypeValue)
+static void copyPrototypeProperties(JSContext *context, Class objcClass, Protocol *protocol, JSValue *prototypeValue)
 {
     // First gather propreties into this list, then handle the methods (capturing the accessor methods).
     struct Property {
@@ -228,13 +239,13 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
         char* getterName;
         char* setterName;
     };
-    __block WTF::Vector<Property> propertyList;
+    __block Vector<Property> propertyList;
 
     // Map recording the methods used as getters/setters.
     NSMutableDictionary *accessorMethods = [NSMutableDictionary dictionary];
 
     // Useful value.
-    JSValueundefined = [JSValue valueWithUndefinedInContext:context];
+    JSValue *undefined = [JSValue valueWithUndefinedInContext:context];
 
     forEachPropertyInProtocol(protocol, ^(objc_property_t property){
         char* getterName = 0;
@@ -263,11 +274,11 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
     for (size_t i = 0; i < propertyList.size(); ++i) {
         Property& property = propertyList[i];
 
-        JSValuegetter = accessorMethods[@(property.getterName)];
+        JSValue *getter = accessorMethods[@(property.getterName)];
         free(property.getterName);
         ASSERT(![getter isUndefined]);
 
-        JSValuesetter = undefined;
+        JSValue *setter = undefined;
         if (property.setterName) {
             setter = accessorMethods[@(property.setterName)];
             free(property.setterName);
@@ -284,15 +295,15 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
 }
 
 @interface JSObjCClassInfo : NSObject {
-    JSContextm_context;
+    JSContext *m_context;
     Class m_class;
     bool m_block;
     JSClassRef m_classRef;
-    JSValue* m_prototype;
-    JSValue* m_constructor;
+    JSC::Weak<JSC::JSObject> m_prototype;
+    JSC::Weak<JSC::JSObject> m_constructor;
 }
 
-- (id)initWithContext:(JSContext*)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo;
+- (id)initWithContext:(JSContext *)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo;
 - (JSValue *)wrapperForObject:(id)object;
 - (JSValue *)constructor;
 
@@ -300,9 +311,11 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
 
 @implementation JSObjCClassInfo
 
-- (id)initWithContext:(JSContext*)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo
+- (id)initWithContext:(JSContext *)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo
 {
-    [super init];
+    self = [super init];
+    if (!self)
+        return nil;
 
     const char* className = class_getName(cls);
     m_context = context;
@@ -314,36 +327,60 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
     definition.parentClass = wrapperClass();
     m_classRef = JSClassCreate(&definition);
 
-    ASSERT((cls == [NSObject class]) == !superClassInfo);
+    [self allocateConstructorAndPrototypeWithSuperClassInfo:superClassInfo];
+
+    return self;
+}
+
+- (void)dealloc
+{
+    JSClassRelease(m_classRef);
+    [super dealloc];
+}
+
+- (void)allocateConstructorAndPrototypeWithSuperClassInfo:(JSObjCClassInfo*)superClassInfo
+{
+    ASSERT((m_class == [NSObject class]) == !superClassInfo);
     if (!superClassInfo) {
-        m_constructor = [context[@"Object"] retain];
-        m_prototype = [m_constructor[@"prototype"] retain];
+        JSContextRef cContext = contextInternalContext(m_context);
+        JSValue *constructor = m_context[@"Object"];
+        m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
+
+        JSValue *prototype = constructor[@"prototype"];
+        m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
     } else {
+        const char* className = class_getName(m_class);
+
         // Create the prototype/constructor pair.
-        m_prototype = createObjectWithCustomBrand(context, [NSString stringWithFormat:@"%sPrototype", className]);
-        m_constructor = createObjectWithCustomBrand(context, [NSString stringWithFormat:@"%sConstructor", className], wrapperClass(), [cls retain]);
-        putNonEnumerable(m_prototype, @"constructor", m_constructor);
-        putNonEnumerable(m_constructor, @"prototype", m_prototype);
-
-        Protocol* exportProtocol = getJSExportProtocol();
-        forEachProtocolImplementingProtocol(cls, exportProtocol, ^(Protocol* protocol){
-            copyPrototypeProperties(context, cls, protocol, m_prototype);
-            copyMethodsToObject(context, cls, protocol, NO, m_constructor);
+        JSValue *prototype = createObjectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sPrototype", className]);
+        JSValue *constructor = createObjectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sConstructor", className], wrapperClass(), [m_class retain]);
+
+        JSContextRef cContext = contextInternalContext(m_context);
+        m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
+        m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
+
+        putNonEnumerable(prototype, @"constructor", constructor);
+        putNonEnumerable(constructor, @"prototype", prototype);
+
+        Protocol *exportProtocol = getJSExportProtocol();
+        forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){
+            copyPrototypeProperties(m_context, m_class, protocol, prototype);
+            copyMethodsToObject(m_context, m_class, protocol, NO, constructor);
         });
 
         // Set [Prototype].
-        m_prototype[@"__proto__"] = superClassInfo->m_prototype;
-    }
+        prototype[@"__proto__"] = [JSValue valueWithValue:toRef(superClassInfo->m_prototype.get()) inContext:m_context];
 
-    return self;
+        [constructor release];
+        [prototype release];
+    }
 }
 
-- (void)dealloc
+- (void)allocateConstructorAndPrototype
 {
-    JSClassRelease(m_classRef);
-    [m_prototype release];
-    [m_constructor release];
-    [super dealloc];
+    ASSERT(!m_constructor.get());
+    ASSERT(!m_prototype.get());
+    [self allocateConstructorAndPrototypeWithSuperClassInfo:[m_context.wrapperMap classInfoForClass:class_getSuperclass(m_class)]];
 }
 
 - (JSValue *)wrapperForObject:(id)object
@@ -355,30 +392,43 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
             return [JSValue valueWithValue:method inContext:m_context];
     }
 
-    JSValueRef prototypeValue = valueInternalValue(m_prototype);
-    ASSERT(JSValueIsObject(contextInternalContext(m_context), prototypeValue));
-    JSObjectRef prototype = JSValueToObject(contextInternalContext(m_context), prototypeValue, 0);
+    JSC::JSObject* prototype = m_prototype.get();
+    if (!prototype) {
+        [self allocateConstructorAndPrototype];
+        prototype = m_prototype.get();
+        ASSERT(prototype);
+    }
 
     JSObjectRef wrapper = JSObjectMake(contextInternalContext(m_context), m_classRef, [object retain]);
-    JSObjectSetPrototype(contextInternalContext(m_context), wrapper, prototype);
+    JSObjectSetPrototype(contextInternalContext(m_context), wrapper, toRef(prototype));
     return [JSValue valueWithValue:wrapper inContext:m_context];
 }
 
 - (JSValue *)constructor
 {
-    return m_constructor;
+    JSC::JSObject* constructor = m_constructor.get();
+    if (!constructor) {
+        [self allocateConstructorAndPrototype];
+        constructor = m_constructor.get();
+        ASSERT(constructor);
+    }
+    return [JSValue valueWithValue:toRef(constructor) inContext:m_context];
 }
 
 @end
 
 @implementation JSWrapperMap {
     JSContext *m_context;
-    NSMutableDictionary* m_classMap;
+    NSMutableDictionary *m_classMap;
+    JSC::WeakGCMap<id, JSC::JSObject> m_cachedWrappers;
 }
 
-- (id)initWithContext:(JSContext*)context
+- (id)initWithContext:(JSContext *)context
 {
-    [super init];
+    self = [super init];
+    if (!self)
+        return nil;
+
     m_context = context;
     m_classMap = [[NSMutableDictionary alloc] init];
     return self;
@@ -408,10 +458,11 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
 
 - (JSValue *)wrapperForObject:(id)object
 {
-    JSValue* wrapper = objc_getAssociatedObject(object, m_context);
-    if (wrapper && wrapper.context)
-        return wrapper;
+    JSC::JSObject* jsWrapper = m_cachedWrappers.get(object);
+    if (jsWrapper)
+        return [JSValue valueWithValue:toRef(jsWrapper) inContext:m_context];
 
+    JSValue *wrapper;
     if (class_isMetaClass(object_getClass(object)))
         wrapper = [[self classInfoForClass:(Class)object] constructor];
     else {
@@ -421,10 +472,12 @@ static void copyPrototypeProperties(JSContext* context, Class objcClass, Protoco
 
     // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105891
     // This general approach to wrapper caching is pretty effective, but there are a couple of problems:
-    // (1) For immortal objects JSValues will effectively leaj and this results in error output being logged - we should avoid adding associated objects to immortal objects.
+    // (1) For immortal objects JSValues will effectively leak and this results in error output being logged - we should avoid adding associated objects to immortal objects.
     // (2) A long lived object may rack up many JSValues. When the contexts are released these will unproctect the associated JavaScript objects,
     //     but still, would probably nicer if we made it so that only one associated object was required, broadcasting object dealloc.
-    objc_setAssociatedObject(object, m_context, wrapper, OBJC_ASSOCIATION_RETAIN);
+    JSC::ExecState* exec = toJS(contextInternalContext(m_context));
+    jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);
+    m_cachedWrappers.set(object, jsWrapper);
     return wrapper;
 }
 
@@ -436,7 +489,7 @@ id tryUnwrapObjcObject(JSGlobalContextRef context, JSValueRef value)
         return nil;
     JSValueRef exception = 0;
     JSObjectRef object = JSValueToObject(context, value, &exception);
-    //ASSERT(!exception);
+    ASSERT(!exception);
     if (JSValueIsObjectOfClass(context, object, wrapperClass()))
         return (id)JSObjectGetPrivate(object);
     if (id target = tryUnwrapBlock(context, object))
@@ -444,19 +497,15 @@ id tryUnwrapObjcObject(JSGlobalContextRef context, JSValueRef value)
     return nil;
 }
 
-ProtocolgetJSExportProtocol()
+Protocol *getJSExportProtocol()
 {
-    static Protocol* protocol = 0;
-    if (!protocol)
-        protocol = objc_getProtocol("JSExport");
+    static Protocol *protocol = objc_getProtocol("JSExport");
     return protocol;
 }
 
 Class getNSBlockClass()
 {
-    static Class cls = 0;
-    if (!cls)
-        cls = objc_getClass("NSBlock");
+    static Class cls = objc_getClass("NSBlock");
     return cls;
 }