Objective-C API: JSObjCClassInfo creates reference cycle with JSContext
[WebKit-https.git] / Source / JavaScriptCore / API / JSWrapperMap.mm
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #import "JavaScriptCore.h"
28
29 #if JS_OBJC_API_ENABLED
30
31 #import "APICast.h"
32 #import "JSContextInternal.h"
33 #import "JSWrapperMap.h"
34 #import "ObjCCallbackFunction.h"
35 #import "ObjcRuntimeExtras.h"
36 #import "Operations.h"
37 #import "WeakGCMap.h"
38 #import <wtf/TCSpinLock.h>
39 #import "wtf/Vector.h"
40
41 @class JSObjCClassInfo;
42
43 @interface JSWrapperMap () 
44
45 - (JSObjCClassInfo*)classInfoForClass:(Class)cls;
46
47 @end
48
49 static void wrapperFinalize(JSObjectRef object)
50 {
51     [(id)JSObjectGetPrivate(object) release];
52 }
53
54 // All wrapper objects and constructor objects derive from this type, so we can detect & unwrap Objective-C instances/Classes.
55 static JSClassRef wrapperClass()
56 {
57     static SpinLock initLock = SPINLOCK_INITIALIZER;
58     SpinLockHolder lockHolder(&initLock);
59
60     static JSClassRef classRef = 0;
61
62     if (!classRef) {
63         JSClassDefinition definition;
64         definition = kJSClassDefinitionEmpty;
65         definition.className = "objc_class";
66         definition.finalize = wrapperFinalize;
67         classRef = JSClassCreate(&definition);
68     }
69
70     return classRef;
71 }
72
73 // Default conversion of selectors to property names.
74 // All semicolons are removed, lowercase letters following a semicolon are capitalized.
75 static NSString *selectorToPropertyName(const char* start)
76 {
77     // Use 'index' to check for colons, if there are non, this is easy!
78     const char* firstColon = index(start, ':');
79     if (!firstColon)
80         return [NSString stringWithUTF8String:start];
81
82     // 'header' is the length of string up to the first colon.
83     size_t header = firstColon - start;
84     // The new string needs to be long enough to hold 'header', plus the remainder of the string, excluding
85     // at least one ':', but including a '\0'. (This is conservative if there are more than one ':').
86     char* buffer = static_cast<char*>(malloc(header + strlen(firstColon + 1) + 1));
87     // Copy 'header' characters, set output to point to the end of this & input to point past the first ':'.
88     memcpy(buffer, start, header);
89     char* output = buffer + header;
90     const char* input = start + header + 1;
91
92     // On entry to the loop, we have already skipped over a ':' from the input.
93     while (true) {
94         char c;
95         // Skip over any additional ':'s. We'll leave c holding the next character after the
96         // last ':', and input pointing past c.
97         while ((c = *(input++)) == ':');
98         // Copy the character, converting to upper case if necessary.
99         // If the character we copy is '\0', then we're done!
100         if (!(*(output++) = toupper(c)))
101             goto done;
102         // Loop over characters other than ':'.
103         while ((c = *(input++)) != ':') {
104             // Copy the character.
105             // If the character we copy is '\0', then we're done!
106             if (!(*(output++) = c))
107                 goto done;
108         }
109         // If we get here, we've consumed a ':' - wash, rinse, repeat.
110     }
111 done:
112     NSString *result = [NSString stringWithUTF8String:buffer];
113     free(buffer);
114     return result;
115 }
116
117 // Make an object that is in all ways are completely vanilla JavaScript object,
118 // other than that it has a native brand set that will be displayed by the default
119 // Object.prototype.toString comversion.
120 static JSValue *createObjectWithCustomBrand(JSContext *context, NSString *brand, JSClassRef parentClass = 0, void* privateData = 0)
121 {
122     JSClassDefinition definition;
123     definition = kJSClassDefinitionEmpty;
124     definition.className = [brand UTF8String];
125     definition.parentClass = parentClass;
126     JSClassRef classRef = JSClassCreate(&definition);
127     JSObjectRef result = JSObjectMake(contextInternalContext(context), classRef, privateData);
128     JSClassRelease(classRef);
129     return [[JSValue alloc] initWithValue:result inContext:context];
130 }
131
132 // Look for @optional properties in the prototype containing a selector to property
133 // name mapping, separated by a __JS_EXPORT_AS__ delimiter.
134 static NSMutableDictionary *createRenameMap(Protocol *protocol, BOOL isInstanceMethod)
135 {
136     NSMutableDictionary *renameMap = [[NSMutableDictionary alloc] init];
137
138     forEachMethodInProtocol(protocol, NO, isInstanceMethod, ^(SEL sel, const char*){
139         NSString *rename = @(sel_getName(sel));
140         NSRange range = [rename rangeOfString:@"__JS_EXPORT_AS__"];
141         if (range.location == NSNotFound)
142             return;
143         NSString *selector = [rename substringToIndex:range.location];
144         NSUInteger begin = range.location + range.length;
145         NSUInteger length = [rename length] - begin - 1;
146         NSString *name = [rename substringWithRange:(NSRange){ begin, length }];
147         renameMap[selector] = name;
148     });
149
150     return renameMap;
151 }
152
153 inline void putNonEnumerable(JSValue *base, NSString *propertyName, JSValue *value)
154 {
155     [base defineProperty:propertyName descriptor:@{
156         JSPropertyDescriptorValueKey: value,
157         JSPropertyDescriptorWritableKey: @YES,
158         JSPropertyDescriptorEnumerableKey: @NO,
159         JSPropertyDescriptorConfigurableKey: @YES
160     }];
161 }
162
163 // This method will iterate over the set of required methods in the protocol, and:
164 //  * Determine a property name (either via a renameMap or default conversion).
165 //  * If an accessorMap is provided, and conatins a this name, store the method in the map.
166 //  * Otherwise, if the object doesn't already conatin a property with name, create it.
167 static void copyMethodsToObject(JSContext *context, Class objcClass, Protocol *protocol, BOOL isInstanceMethod, JSValue *object, NSMutableDictionary *accessorMethods = nil)
168 {
169     NSMutableDictionary *renameMap = createRenameMap(protocol, isInstanceMethod);
170
171     forEachMethodInProtocol(protocol, YES, isInstanceMethod, ^(SEL sel, const char* types){
172         const char* nameCStr = sel_getName(sel);
173         NSString *name = @(nameCStr);
174         if (accessorMethods && accessorMethods[name]) {
175             JSObjectRef method = objCCallbackFunctionForMethod(context, objcClass, protocol, isInstanceMethod, sel, types);
176             if (!method)
177                 return;
178             accessorMethods[name] = [JSValue valueWithValue:method inContext:context];
179         } else {
180             name = renameMap[name];
181             if (!name)
182                 name = selectorToPropertyName(nameCStr);
183             if ([object hasProperty:name])
184                 return;
185             JSObjectRef method = objCCallbackFunctionForMethod(context, objcClass, protocol, isInstanceMethod, sel, types);
186             if (method)
187                 putNonEnumerable(object, name, [JSValue valueWithValue:method inContext:context]);
188         }
189     });
190
191     [renameMap release];
192 }
193
194 static bool parsePropertyAttributes(objc_property_t property, char*& getterName, char*& setterName)
195 {
196     bool readonly = false;
197     unsigned attributeCount;
198     objc_property_attribute_t* attributes = property_copyAttributeList(property, &attributeCount);
199     if (attributeCount) {
200         for (unsigned i = 0; i < attributeCount; ++i) {
201             switch (*(attributes[i].name)) {
202             case 'G':
203                 getterName = strdup(attributes[i].value);
204                 break;
205             case 'S':
206                 setterName = strdup(attributes[i].value);
207                 break;
208             case 'R':
209                 readonly = true;
210                 break;
211             default:
212                 break;
213             }
214         }
215         free(attributes);
216     }
217     return readonly;
218 }
219
220 static char* makeSetterName(const char* name)
221 {
222     size_t nameLength = strlen(name);
223     char* setterName = (char*)malloc(nameLength + 5); // "set" Name ":\0"
224     setterName[0] = 's';
225     setterName[1] = 'e';
226     setterName[2] = 't';
227     setterName[3] = toupper(*name);
228     memcpy(setterName + 4, name + 1, nameLength - 1);
229     setterName[nameLength + 3] = ':';
230     setterName[nameLength + 4] = '\0';
231     return setterName;
232 }
233
234 static void copyPrototypeProperties(JSContext *context, Class objcClass, Protocol *protocol, JSValue *prototypeValue)
235 {
236     // First gather propreties into this list, then handle the methods (capturing the accessor methods).
237     struct Property {
238         const char* name;
239         char* getterName;
240         char* setterName;
241     };
242     __block Vector<Property> propertyList;
243
244     // Map recording the methods used as getters/setters.
245     NSMutableDictionary *accessorMethods = [NSMutableDictionary dictionary];
246
247     // Useful value.
248     JSValue *undefined = [JSValue valueWithUndefinedInContext:context];
249
250     forEachPropertyInProtocol(protocol, ^(objc_property_t property){
251         char* getterName = 0;
252         char* setterName = 0;
253         bool readonly = parsePropertyAttributes(property, getterName, setterName);
254         const char* name = property_getName(property);
255
256         // Add the names of the getter & setter methods to 
257         if (!getterName)
258             getterName = strdup(name);
259         accessorMethods[@(getterName)] = undefined;
260         if (!readonly) {
261             if (!setterName)
262                 setterName = makeSetterName(name);
263             accessorMethods[@(setterName)] = undefined;
264         }
265
266         // Add the properties to a list.
267         propertyList.append((Property){ name, getterName, setterName });
268     });
269
270     // Copy methods to the prototype, capturing accessors in the accessorMethods map.
271     copyMethodsToObject(context, objcClass, protocol, YES, prototypeValue, accessorMethods);
272
273     // Iterate the propertyList & generate accessor properties.
274     for (size_t i = 0; i < propertyList.size(); ++i) {
275         Property& property = propertyList[i];
276
277         JSValue *getter = accessorMethods[@(property.getterName)];
278         free(property.getterName);
279         ASSERT(![getter isUndefined]);
280
281         JSValue *setter = undefined;
282         if (property.setterName) {
283             setter = accessorMethods[@(property.setterName)];
284             free(property.setterName);
285             ASSERT(![setter isUndefined]);
286         }
287         
288         [prototypeValue defineProperty:@(property.name) descriptor:@{
289             JSPropertyDescriptorGetKey: getter,
290             JSPropertyDescriptorSetKey: setter,
291             JSPropertyDescriptorEnumerableKey: @NO,
292             JSPropertyDescriptorConfigurableKey: @YES
293         }];
294     }
295 }
296
297 @interface JSObjCClassInfo : NSObject {
298     JSContext *m_context;
299     Class m_class;
300     bool m_block;
301     JSClassRef m_classRef;
302     JSC::Weak<JSC::JSObject> m_prototype;
303     JSC::Weak<JSC::JSObject> m_constructor;
304 }
305
306 - (id)initWithContext:(JSContext *)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo;
307 - (JSValue *)wrapperForObject:(id)object;
308 - (JSValue *)constructor;
309
310 @end
311
312 @implementation JSObjCClassInfo
313
314 - (id)initWithContext:(JSContext *)context forClass:(Class)cls superClassInfo:(JSObjCClassInfo*)superClassInfo
315 {
316     self = [super init];
317     if (!self)
318         return nil;
319
320     const char* className = class_getName(cls);
321     m_context = context;
322     m_class = cls;
323     m_block = [cls isSubclassOfClass:getNSBlockClass()];
324     JSClassDefinition definition;
325     definition = kJSClassDefinitionEmpty;
326     definition.className = className;
327     definition.parentClass = wrapperClass();
328     m_classRef = JSClassCreate(&definition);
329
330     [self allocateConstructorAndPrototypeWithSuperClassInfo:superClassInfo];
331
332     return self;
333 }
334
335 - (void)dealloc
336 {
337     JSClassRelease(m_classRef);
338     [super dealloc];
339 }
340
341 - (void)allocateConstructorAndPrototypeWithSuperClassInfo:(JSObjCClassInfo*)superClassInfo
342 {
343     ASSERT((m_class == [NSObject class]) == !superClassInfo);
344     if (!superClassInfo) {
345         JSContextRef cContext = contextInternalContext(m_context);
346         JSValue *constructor = m_context[@"Object"];
347         m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
348
349         JSValue *prototype = constructor[@"prototype"];
350         m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
351     } else {
352         const char* className = class_getName(m_class);
353
354         // Create the prototype/constructor pair.
355         JSValue *prototype = createObjectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sPrototype", className]);
356         JSValue *constructor = createObjectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sConstructor", className], wrapperClass(), [m_class retain]);
357
358         JSContextRef cContext = contextInternalContext(m_context);
359         m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
360         m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
361
362         putNonEnumerable(prototype, @"constructor", constructor);
363         putNonEnumerable(constructor, @"prototype", prototype);
364
365         Protocol *exportProtocol = getJSExportProtocol();
366         forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){
367             copyPrototypeProperties(m_context, m_class, protocol, prototype);
368             copyMethodsToObject(m_context, m_class, protocol, NO, constructor);
369         });
370
371         // Set [Prototype].
372         prototype[@"__proto__"] = [JSValue valueWithValue:toRef(superClassInfo->m_prototype.get()) inContext:m_context];
373
374         [constructor release];
375         [prototype release];
376     }
377 }
378
379 - (void)allocateConstructorAndPrototype
380 {
381     ASSERT(!m_constructor.get());
382     ASSERT(!m_prototype.get());
383     [self allocateConstructorAndPrototypeWithSuperClassInfo:[m_context.wrapperMap classInfoForClass:class_getSuperclass(m_class)]];
384 }
385
386 - (JSValue *)wrapperForObject:(id)object
387 {
388     ASSERT([object isKindOfClass:m_class]);
389     ASSERT(m_block == [object isKindOfClass:getNSBlockClass()]);
390     if (m_block) {
391         if (JSObjectRef method = objCCallbackFunctionForBlock(m_context, object))
392             return [JSValue valueWithValue:method inContext:m_context];
393     }
394
395     JSC::JSObject* prototype = m_prototype.get();
396     if (!prototype) {
397         [self allocateConstructorAndPrototype];
398         prototype = m_prototype.get();
399         ASSERT(prototype);
400     }
401
402     JSObjectRef wrapper = JSObjectMake(contextInternalContext(m_context), m_classRef, [object retain]);
403     JSObjectSetPrototype(contextInternalContext(m_context), wrapper, toRef(prototype));
404     return [JSValue valueWithValue:wrapper inContext:m_context];
405 }
406
407 - (JSValue *)constructor
408 {
409     JSC::JSObject* constructor = m_constructor.get();
410     if (!constructor) {
411         [self allocateConstructorAndPrototype];
412         constructor = m_constructor.get();
413         ASSERT(constructor);
414     }
415     return [JSValue valueWithValue:toRef(constructor) inContext:m_context];
416 }
417
418 @end
419
420 @implementation JSWrapperMap {
421     JSContext *m_context;
422     NSMutableDictionary *m_classMap;
423     JSC::WeakGCMap<id, JSC::JSObject> m_cachedWrappers;
424 }
425
426 - (id)initWithContext:(JSContext *)context
427 {
428     self = [super init];
429     if (!self)
430         return nil;
431
432     m_context = context;
433     m_classMap = [[NSMutableDictionary alloc] init];
434     return self;
435 }
436
437 - (void)dealloc
438 {
439     [m_classMap release];
440     [super dealloc];
441 }
442
443 - (JSObjCClassInfo*)classInfoForClass:(Class)cls
444 {
445     if (!cls)
446         return nil;
447
448     // Check if we've already created a JSObjCClassInfo for this Class.
449     if (JSObjCClassInfo* classInfo = (JSObjCClassInfo*)m_classMap[cls])
450         return classInfo;
451
452     // Skip internal classes begining with '_' - just copy link to the parent class's info.
453     if ('_' == *class_getName(cls))
454         return m_classMap[cls] = [self classInfoForClass:class_getSuperclass(cls)];
455
456     return m_classMap[cls] = [[[JSObjCClassInfo alloc] initWithContext:m_context forClass:cls superClassInfo:[self classInfoForClass:class_getSuperclass(cls)]] autorelease];
457 }
458
459 - (JSValue *)wrapperForObject:(id)object
460 {
461     JSC::JSObject* jsWrapper = m_cachedWrappers.get(object);
462     if (jsWrapper)
463         return [JSValue valueWithValue:toRef(jsWrapper) inContext:m_context];
464
465     JSValue *wrapper;
466     if (class_isMetaClass(object_getClass(object)))
467         wrapper = [[self classInfoForClass:(Class)object] constructor];
468     else {
469         JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];
470         wrapper = [classInfo wrapperForObject:object];
471     }
472
473     // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105891
474     // This general approach to wrapper caching is pretty effective, but there are a couple of problems:
475     // (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.
476     // (2) A long lived object may rack up many JSValues. When the contexts are released these will unproctect the associated JavaScript objects,
477     //     but still, would probably nicer if we made it so that only one associated object was required, broadcasting object dealloc.
478     JSC::ExecState* exec = toJS(contextInternalContext(m_context));
479     jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);
480     m_cachedWrappers.set(object, jsWrapper);
481     return wrapper;
482 }
483
484 @end
485
486 id tryUnwrapObjcObject(JSGlobalContextRef context, JSValueRef value)
487 {
488     if (!JSValueIsObject(context, value))
489         return nil;
490     JSValueRef exception = 0;
491     JSObjectRef object = JSValueToObject(context, value, &exception);
492     ASSERT(!exception);
493     if (JSValueIsObjectOfClass(context, object, wrapperClass()))
494         return (id)JSObjectGetPrivate(object);
495     if (id target = tryUnwrapBlock(context, object))
496         return target;
497     return nil;
498 }
499
500 Protocol *getJSExportProtocol()
501 {
502     static Protocol *protocol = objc_getProtocol("JSExport");
503     return protocol;
504 }
505
506 Class getNSBlockClass()
507 {
508     static Class cls = objc_getClass("NSBlock");
509     return cls;
510 }
511
512 #endif