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