]> git.saurik.com Git - apple/javascriptcore.git/blob - API/JSWrapperMap.mm
4dde1a6597b670daa09ea7076d515e6793845302
[apple/javascriptcore.git] / 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 JSC_OBJC_API_ENABLED
30
31 #import "APICast.h"
32 #import "APIShims.h"
33 #import "JSAPIWrapperObject.h"
34 #import "JSCallbackObject.h"
35 #import "JSContextInternal.h"
36 #import "JSWrapperMap.h"
37 #import "ObjCCallbackFunction.h"
38 #import "ObjcRuntimeExtras.h"
39 #import "Operations.h"
40 #import "WeakGCMap.h"
41 #import <wtf/TCSpinLock.h>
42 #import <wtf/Vector.h>
43
44 @class JSObjCClassInfo;
45
46 @interface JSWrapperMap ()
47
48 - (JSObjCClassInfo*)classInfoForClass:(Class)cls;
49
50 @end
51
52 // Default conversion of selectors to property names.
53 // All semicolons are removed, lowercase letters following a semicolon are capitalized.
54 static NSString *selectorToPropertyName(const char* start)
55 {
56 // Use 'index' to check for colons, if there are none, this is easy!
57 const char* firstColon = index(start, ':');
58 if (!firstColon)
59 return [NSString stringWithUTF8String:start];
60
61 // 'header' is the length of string up to the first colon.
62 size_t header = firstColon - start;
63 // The new string needs to be long enough to hold 'header', plus the remainder of the string, excluding
64 // at least one ':', but including a '\0'. (This is conservative if there are more than one ':').
65 char* buffer = static_cast<char*>(malloc(header + strlen(firstColon + 1) + 1));
66 // Copy 'header' characters, set output to point to the end of this & input to point past the first ':'.
67 memcpy(buffer, start, header);
68 char* output = buffer + header;
69 const char* input = start + header + 1;
70
71 // On entry to the loop, we have already skipped over a ':' from the input.
72 while (true) {
73 char c;
74 // Skip over any additional ':'s. We'll leave c holding the next character after the
75 // last ':', and input pointing past c.
76 while ((c = *(input++)) == ':');
77 // Copy the character, converting to upper case if necessary.
78 // If the character we copy is '\0', then we're done!
79 if (!(*(output++) = toupper(c)))
80 goto done;
81 // Loop over characters other than ':'.
82 while ((c = *(input++)) != ':') {
83 // Copy the character.
84 // If the character we copy is '\0', then we're done!
85 if (!(*(output++) = c))
86 goto done;
87 }
88 // If we get here, we've consumed a ':' - wash, rinse, repeat.
89 }
90 done:
91 NSString *result = [NSString stringWithUTF8String:buffer];
92 free(buffer);
93 return result;
94 }
95
96 static JSObjectRef makeWrapper(JSContextRef ctx, JSClassRef jsClass, id wrappedObject)
97 {
98 JSC::ExecState* exec = toJS(ctx);
99 JSC::APIEntryShim entryShim(exec);
100
101 ASSERT(jsClass);
102 JSC::JSCallbackObject<JSC::JSAPIWrapperObject>* object = JSC::JSCallbackObject<JSC::JSAPIWrapperObject>::create(exec, exec->lexicalGlobalObject(), exec->lexicalGlobalObject()->objcWrapperObjectStructure(), jsClass, 0);
103 object->setWrappedObject(wrappedObject);
104 if (JSC::JSObject* prototype = jsClass->prototype(exec))
105 object->setPrototype(exec->vm(), prototype);
106
107 return toRef(object);
108 }
109
110 // Make an object that is in all ways a completely vanilla JavaScript object,
111 // other than that it has a native brand set that will be displayed by the default
112 // Object.prototype.toString conversion.
113 static JSValue *objectWithCustomBrand(JSContext *context, NSString *brand, Class cls = 0)
114 {
115 JSClassDefinition definition;
116 definition = kJSClassDefinitionEmpty;
117 definition.className = [brand UTF8String];
118 JSClassRef classRef = JSClassCreate(&definition);
119 JSObjectRef result = makeWrapper([context JSGlobalContextRef], classRef, cls);
120 JSClassRelease(classRef);
121 return [JSValue valueWithJSValueRef: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 contains this name, store the method in the map.
158 // * Otherwise, if the object doesn't already contain 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 valueWithJSValueRef: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 valueWithJSValueRef: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 JSC::Weak<JSC::JSObject> m_prototype;
295 JSC::Weak<JSC::JSObject> 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 m_classRef = JSClassCreate(&definition);
320
321 [self allocateConstructorAndPrototypeWithSuperClassInfo:superClassInfo];
322
323 return self;
324 }
325
326 - (void)dealloc
327 {
328 JSClassRelease(m_classRef);
329 [super dealloc];
330 }
331
332 - (void)allocateConstructorAndPrototypeWithSuperClassInfo:(JSObjCClassInfo*)superClassInfo
333 {
334 ASSERT(!m_constructor || !m_prototype);
335 ASSERT((m_class == [NSObject class]) == !superClassInfo);
336 if (!superClassInfo) {
337 JSContextRef cContext = [m_context JSGlobalContextRef];
338 JSValue *constructor = m_context[@"Object"];
339 if (!m_constructor)
340 m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
341
342 if (!m_prototype) {
343 JSValue *prototype = constructor[@"prototype"];
344 m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
345 }
346 } else {
347 const char* className = class_getName(m_class);
348
349 // Create or grab the prototype/constructor pair.
350 JSValue *prototype;
351 JSValue *constructor;
352 if (m_prototype)
353 prototype = [JSValue valueWithJSValueRef:toRef(m_prototype.get()) inContext:m_context];
354 else
355 prototype = objectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sPrototype", className]);
356
357 if (m_constructor)
358 constructor = [JSValue valueWithJSValueRef:toRef(m_constructor.get()) inContext:m_context];
359 else
360 constructor = objectWithCustomBrand(m_context, [NSString stringWithFormat:@"%sConstructor", className], m_class);
361
362 JSContextRef cContext = [m_context JSGlobalContextRef];
363 m_prototype = toJS(JSValueToObject(cContext, valueInternalValue(prototype), 0));
364 m_constructor = toJS(JSValueToObject(cContext, valueInternalValue(constructor), 0));
365
366 putNonEnumerable(prototype, @"constructor", constructor);
367 putNonEnumerable(constructor, @"prototype", prototype);
368
369 Protocol *exportProtocol = getJSExportProtocol();
370 forEachProtocolImplementingProtocol(m_class, exportProtocol, ^(Protocol *protocol){
371 copyPrototypeProperties(m_context, m_class, protocol, prototype);
372 copyMethodsToObject(m_context, m_class, protocol, NO, constructor);
373 });
374
375 // Set [Prototype].
376 JSObjectSetPrototype([m_context JSGlobalContextRef], toRef(m_prototype.get()), toRef(superClassInfo->m_prototype.get()));
377 }
378 }
379
380 - (void)reallocateConstructorAndOrPrototype
381 {
382 [self allocateConstructorAndPrototypeWithSuperClassInfo:[m_context.wrapperMap classInfoForClass:class_getSuperclass(m_class)]];
383 }
384
385 - (JSValue *)wrapperForObject:(id)object
386 {
387 ASSERT([object isKindOfClass:m_class]);
388 ASSERT(m_block == [object isKindOfClass:getNSBlockClass()]);
389 if (m_block) {
390 if (JSObjectRef method = objCCallbackFunctionForBlock(m_context, object))
391 return [JSValue valueWithJSValueRef:method inContext:m_context];
392 }
393
394 if (!m_prototype)
395 [self reallocateConstructorAndOrPrototype];
396 ASSERT(!!m_prototype);
397
398 JSObjectRef wrapper = makeWrapper([m_context JSGlobalContextRef], m_classRef, object);
399 JSObjectSetPrototype([m_context JSGlobalContextRef], wrapper, toRef(m_prototype.get()));
400 return [JSValue valueWithJSValueRef:wrapper inContext:m_context];
401 }
402
403 - (JSValue *)constructor
404 {
405 if (!m_constructor)
406 [self reallocateConstructorAndOrPrototype];
407 ASSERT(!!m_constructor);
408 return [JSValue valueWithJSValueRef:toRef(m_constructor.get()) inContext:m_context];
409 }
410
411 @end
412
413 @implementation JSWrapperMap {
414 JSContext *m_context;
415 NSMutableDictionary *m_classMap;
416 JSC::WeakGCMap<id, JSC::JSObject> m_cachedJSWrappers;
417 NSMapTable *m_cachedObjCWrappers;
418 }
419
420 - (id)initWithContext:(JSContext *)context
421 {
422 self = [super init];
423 if (!self)
424 return nil;
425
426 NSPointerFunctionsOptions keyOptions = NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality;
427 NSPointerFunctionsOptions valueOptions = NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPersonality;
428 m_cachedObjCWrappers = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:valueOptions capacity:0];
429
430 m_context = context;
431 m_classMap = [[NSMutableDictionary alloc] init];
432 return self;
433 }
434
435 - (void)dealloc
436 {
437 [m_cachedObjCWrappers release];
438 [m_classMap release];
439 [super dealloc];
440 }
441
442 - (JSObjCClassInfo*)classInfoForClass:(Class)cls
443 {
444 if (!cls)
445 return nil;
446
447 // Check if we've already created a JSObjCClassInfo for this Class.
448 if (JSObjCClassInfo* classInfo = (JSObjCClassInfo*)m_classMap[cls])
449 return classInfo;
450
451 // Skip internal classes beginning with '_' - just copy link to the parent class's info.
452 if ('_' == *class_getName(cls))
453 return m_classMap[cls] = [self classInfoForClass:class_getSuperclass(cls)];
454
455 return m_classMap[cls] = [[[JSObjCClassInfo alloc] initWithContext:m_context forClass:cls superClassInfo:[self classInfoForClass:class_getSuperclass(cls)]] autorelease];
456 }
457
458 - (JSValue *)jsWrapperForObject:(id)object
459 {
460 JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object);
461 if (jsWrapper)
462 return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];
463
464 JSValue *wrapper;
465 if (class_isMetaClass(object_getClass(object)))
466 wrapper = [[self classInfoForClass:(Class)object] constructor];
467 else {
468 JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];
469 wrapper = [classInfo wrapperForObject:object];
470 }
471
472 // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105891
473 // This general approach to wrapper caching is pretty effective, but there are a couple of problems:
474 // (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.
475 // (2) A long lived object may rack up many JSValues. When the contexts are released these will unprotect the associated JavaScript objects,
476 // but still, would probably nicer if we made it so that only one associated object was required, broadcasting object dealloc.
477 JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]);
478 jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);
479 m_cachedJSWrappers.set(object, jsWrapper);
480 return wrapper;
481 }
482
483 - (JSValue *)objcWrapperForJSValueRef:(JSValueRef)value
484 {
485 JSValue *wrapper = static_cast<JSValue *>(NSMapGet(m_cachedObjCWrappers, value));
486 if (!wrapper) {
487 wrapper = [[[JSValue alloc] initWithValue:value inContext:m_context] autorelease];
488 NSMapInsert(m_cachedObjCWrappers, value, wrapper);
489 }
490 return wrapper;
491 }
492
493 @end
494
495 id tryUnwrapObjcObject(JSGlobalContextRef context, JSValueRef value)
496 {
497 if (!JSValueIsObject(context, value))
498 return nil;
499 JSValueRef exception = 0;
500 JSObjectRef object = JSValueToObject(context, value, &exception);
501 ASSERT(!exception);
502 if (toJS(object)->inherits(&JSC::JSCallbackObject<JSC::JSAPIWrapperObject>::s_info))
503 return (id)JSC::jsCast<JSC::JSAPIWrapperObject*>(toJS(object))->wrappedObject();
504 if (id target = tryUnwrapBlock(object))
505 return target;
506 return nil;
507 }
508
509 Protocol *getJSExportProtocol()
510 {
511 static Protocol *protocol = objc_getProtocol("JSExport");
512 return protocol;
513 }
514
515 Class getNSBlockClass()
516 {
517 static Class cls = objc_getClass("NSBlock");
518 return cls;
519 }
520
521 #endif