/* * Copyright (C) 2013-2018 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "APICast.h" #import "DateInstance.h" #import "Error.h" #import "Exception.h" #import "JavaScriptCore.h" #import "JSContextInternal.h" #import "JSObjectRefPrivate.h" #import "JSVirtualMachineInternal.h" #import "JSValueInternal.h" #import "JSValuePrivate.h" #import "JSWrapperMap.h" #import "MarkedJSValueRefArray.h" #import "ObjcRuntimeExtras.h" #import "JSCInlines.h" #import "JSCJSValue.h" #import "Strong.h" #import "StrongInlines.h" #import #import #import #import #import #import #import #if ENABLE(REMOTE_INSPECTOR) #import "CallFrame.h" #import "JSGlobalObject.h" #import "JSGlobalObjectInspectorController.h" #endif #if JSC_OBJC_API_ENABLED NSString * const JSPropertyDescriptorWritableKey = @"writable"; NSString * const JSPropertyDescriptorEnumerableKey = @"enumerable"; NSString * const JSPropertyDescriptorConfigurableKey = @"configurable"; NSString * const JSPropertyDescriptorValueKey = @"value"; NSString * const JSPropertyDescriptorGetKey = @"get"; NSString * const JSPropertyDescriptorSetKey = @"set"; #if defined(DARLING) && __i386__ @implementation JSValue #else @implementation JSValue { JSValueRef m_value; } #endif - (void)dealloc { JSValueUnprotect([_context JSGlobalContextRef], m_value); [_context release]; _context = nil; [super dealloc]; } - (NSString *)description { if (id wrapped = tryUnwrapObjcObject([_context JSGlobalContextRef], m_value)) return [wrapped description]; return [self toString]; } - (JSValueRef)JSValueRef { return m_value; } + (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context { return [JSValue valueWithJSValueRef:objectToValue(context, value) inContext:context]; } + (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeBoolean([context JSGlobalContextRef], value) inContext:context]; } + (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context]; } + (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context]; } + (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeNumber([context JSGlobalContextRef], value) inContext:context]; } + (JSValue *)valueWithNewObjectInContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSObjectMake([context JSGlobalContextRef], 0, 0) inContext:context]; } + (JSValue *)valueWithNewArrayInContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSObjectMakeArray([context JSGlobalContextRef], 0, NULL, 0) inContext:context]; } + (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context { auto patternString = OpaqueJSString::tryCreate(pattern); auto flagsString = OpaqueJSString::tryCreate(flags); JSValueRef arguments[2] = { JSValueMakeString([context JSGlobalContextRef], patternString.get()), JSValueMakeString([context JSGlobalContextRef], flagsString.get()) }; return [JSValue valueWithJSValueRef:JSObjectMakeRegExp([context JSGlobalContextRef], 2, arguments, 0) inContext:context]; } + (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context { auto string = OpaqueJSString::tryCreate(message); JSValueRef argument = JSValueMakeString([context JSGlobalContextRef], string.get()); return [JSValue valueWithJSValueRef:JSObjectMakeError([context JSGlobalContextRef], 1, &argument, 0) inContext:context]; } + (JSValue *)valueWithNullInContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeNull([context JSGlobalContextRef]) inContext:context]; } + (JSValue *)valueWithUndefinedInContext:(JSContext *)context { return [JSValue valueWithJSValueRef:JSValueMakeUndefined([context JSGlobalContextRef]) inContext:context]; } + (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context { auto string = OpaqueJSString::tryCreate(description); return [JSValue valueWithJSValueRef:JSValueMakeSymbol([context JSGlobalContextRef], string.get()) inContext:context]; } + (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *, JSValue *))executor { JSObjectRef resolve; JSObjectRef reject; JSValueRef exception = nullptr; JSObjectRef promise = JSObjectMakeDeferredPromise([context JSGlobalContextRef], &resolve, &reject, &exception); if (exception) { [context notifyException:exception]; return [JSValue valueWithUndefinedInContext:context]; } JSValue *result = [JSValue valueWithJSValueRef:promise inContext:context]; JSValue *rejection = [JSValue valueWithJSValueRef:reject inContext:context]; CallbackData callbackData; const size_t argumentCount = 2; JSValueRef arguments[argumentCount]; arguments[0] = resolve; arguments[1] = reject; [context beginCallbackWithData:&callbackData calleeValue:nullptr thisValue:promise argumentCount:argumentCount arguments:arguments]; executor([JSValue valueWithJSValueRef:resolve inContext:context], rejection); if (context.exception) [rejection callWithArguments:@[context.exception]]; [context endCallbackWithData:&callbackData]; return result; } + (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context { return [JSValue valueWithNewPromiseInContext:context fromExecutor:^(JSValue *resolve, JSValue *) { [resolve callWithArguments:@[result]]; }]; } + (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context { return [JSValue valueWithNewPromiseInContext:context fromExecutor:^(JSValue *, JSValue *reject) { [reject callWithArguments:@[reason]]; }]; } - (id)toObject { return valueToObject(_context, m_value); } - (id)toObjectOfClass:(Class)expectedClass { id result = [self toObject]; return [result isKindOfClass:expectedClass] ? result : nil; } - (BOOL)toBool { return JSValueToBoolean([_context JSGlobalContextRef], m_value); } - (double)toDouble { JSValueRef exception = 0; double result = JSValueToNumber([_context JSGlobalContextRef], m_value, &exception); if (exception) { [_context notifyException:exception]; return std::numeric_limits::quiet_NaN(); } return result; } - (int32_t)toInt32 { return JSC::toInt32([self toDouble]); } - (uint32_t)toUInt32 { return JSC::toUInt32([self toDouble]); } - (NSNumber *)toNumber { JSValueRef exception = 0; id result = valueToNumber([_context JSGlobalContextRef], m_value, &exception); if (exception) [_context notifyException:exception]; return result; } - (NSString *)toString { JSValueRef exception = 0; id result = valueToString([_context JSGlobalContextRef], m_value, &exception); if (exception) [_context notifyException:exception]; return result; } - (NSDate *)toDate { JSValueRef exception = 0; id result = valueToDate([_context JSGlobalContextRef], m_value, &exception); if (exception) [_context notifyException:exception]; return result; } - (NSArray *)toArray { JSValueRef exception = 0; id result = valueToArray([_context JSGlobalContextRef], m_value, &exception); if (exception) [_context notifyException:exception]; return result; } - (NSDictionary *)toDictionary { JSValueRef exception = 0; id result = valueToDictionary([_context JSGlobalContextRef], m_value, &exception); if (exception) [_context notifyException:exception]; return result; } template inline Expected performPropertyOperation(NSStringFunction stringFunction, JSValueFunction jsFunction, JSValue* value, id propertyKey, Types... arguments) { JSContext* context = [value context]; JSValueRef exception = nullptr; JSObjectRef object = JSValueToObject([context JSGlobalContextRef], [value JSValueRef], &exception); if (exception) return Unexpected(exception); Result result; // If it's a NSString already, reduce indirection and just pass the NSString. if ([propertyKey isKindOfClass:[NSString class]]) { auto name = OpaqueJSString::tryCreate((NSString *)propertyKey); result = stringFunction([context JSGlobalContextRef], object, name.get(), arguments..., &exception); } else result = jsFunction([context JSGlobalContextRef], object, [[JSValue valueWithObject:propertyKey inContext:context] JSValueRef], arguments..., &exception); return Expected(result); } - (JSValue *)valueForProperty:(id)key { auto result = performPropertyOperation(JSObjectGetProperty, JSObjectGetPropertyForKey, self, key); if (!result) return [_context valueFromNotifyException:result.error()]; return [JSValue valueWithJSValueRef:result.value() inContext:_context]; } - (void)setValue:(id)value forProperty:(JSValueProperty)key { // We need Unit business because void can't be assigned to in performPropertyOperation and I don't want to duplicate the code... using Unit = std::tuple<>; auto stringSetProperty = [] (auto... args) -> Unit { JSObjectSetProperty(args...); return { }; }; auto jsValueSetProperty = [] (auto... args) -> Unit { JSObjectSetPropertyForKey(args...); return { }; }; auto result = performPropertyOperation(stringSetProperty, jsValueSetProperty, self, key, objectToValue(_context, value), kJSPropertyAttributeNone); if (!result) { [_context notifyException:result.error()]; return; } } - (BOOL)deleteProperty:(JSValueProperty)key { Expected result = performPropertyOperation(JSObjectDeleteProperty, JSObjectDeletePropertyForKey, self, key); if (!result) return [_context boolFromNotifyException:result.error()]; return result.value(); } - (BOOL)hasProperty:(JSValueProperty)key { // The C-api doesn't return an exception value for the string version of has property. auto stringHasProperty = [] (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef*) -> BOOL { return JSObjectHasProperty(ctx, object, propertyName); }; Expected result = performPropertyOperation(stringHasProperty, JSObjectHasPropertyForKey, self, key); if (!result) return [_context boolFromNotifyException:result.error()]; return result.value(); } - (void)defineProperty:(JSValueProperty)key descriptor:(id)descriptor { [[_context globalObject][@"Object"] invokeMethod:@"defineProperty" withArguments:@[ self, key, descriptor ]]; } - (JSValue *)valueAtIndex:(NSUInteger)index { // Properties that are higher than an unsigned value can hold are converted to a double then inserted as a normal property. // Indices that are bigger than the max allowed index size (UINT_MAX - 1) will be handled internally in get(). if (index != (unsigned)index) return [self valueForProperty:[[JSValue valueWithDouble:index inContext:_context] toString]]; JSValueRef exception = 0; JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception); if (exception) return [_context valueFromNotifyException:exception]; JSValueRef result = JSObjectGetPropertyAtIndex([_context JSGlobalContextRef], object, (unsigned)index, &exception); if (exception) return [_context valueFromNotifyException:exception]; return [JSValue valueWithJSValueRef:result inContext:_context]; } - (void)setValue:(id)value atIndex:(NSUInteger)index { // Properties that are higher than an unsigned value can hold are converted to a double, then inserted as a normal property. // Indices that are bigger than the max allowed index size (UINT_MAX - 1) will be handled internally in putByIndex(). if (index != (unsigned)index) return [self setValue:value forProperty:[[JSValue valueWithDouble:index inContext:_context] toString]]; JSValueRef exception = 0; JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception); if (exception) { [_context notifyException:exception]; return; } JSObjectSetPropertyAtIndex([_context JSGlobalContextRef], object, (unsigned)index, objectToValue(_context, value), &exception); if (exception) { [_context notifyException:exception]; return; } } - (BOOL)isUndefined { #if !CPU(ADDRESS64) return JSValueIsUndefined([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isUndefined(); #endif } - (BOOL)isNull { #if !CPU(ADDRESS64) return JSValueIsNull([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isNull(); #endif } - (BOOL)isBoolean { #if !CPU(ADDRESS64) return JSValueIsBoolean([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isBoolean(); #endif } - (BOOL)isNumber { #if !CPU(ADDRESS64) return JSValueIsNumber([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isNumber(); #endif } - (BOOL)isString { #if !CPU(ADDRESS64) return JSValueIsString([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isString(); #endif } - (BOOL)isObject { #if !CPU(ADDRESS64) return JSValueIsObject([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isObject(); #endif } - (BOOL)isSymbol { #if !CPU(ADDRESS64) return JSValueIsSymbol([_context JSGlobalContextRef], m_value); #else return toJS(m_value).isSymbol(); #endif } - (BOOL)isArray { return JSValueIsArray([_context JSGlobalContextRef], m_value); } - (BOOL)isDate { return JSValueIsDate([_context JSGlobalContextRef], m_value); } - (BOOL)isEqualToObject:(id)value { return JSValueIsStrictEqual([_context JSGlobalContextRef], m_value, objectToValue(_context, value)); } - (BOOL)isEqualWithTypeCoercionToObject:(id)value { JSValueRef exception = 0; BOOL result = JSValueIsEqual([_context JSGlobalContextRef], m_value, objectToValue(_context, value), &exception); if (exception) return [_context boolFromNotifyException:exception]; return result; } - (BOOL)isInstanceOf:(id)value { JSValueRef exception = 0; JSObjectRef constructor = JSValueToObject([_context JSGlobalContextRef], objectToValue(_context, value), &exception); if (exception) return [_context boolFromNotifyException:exception]; BOOL result = JSValueIsInstanceOfConstructor([_context JSGlobalContextRef], m_value, constructor, &exception); if (exception) return [_context boolFromNotifyException:exception]; return result; } - (JSValue *)callWithArguments:(NSArray *)argumentArray { JSC::JSGlobalObject* globalObject = toJS([_context JSGlobalContextRef]); JSC::VM& vm = globalObject->vm(); JSC::JSLockHolder locker(vm); NSUInteger argumentCount = [argumentArray count]; JSC::MarkedJSValueRefArray arguments([_context JSGlobalContextRef], argumentCount); for (unsigned i = 0; i < argumentCount; ++i) arguments[i] = objectToValue(_context, [argumentArray objectAtIndex:i]); JSValueRef exception = 0; JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception); if (exception) return [_context valueFromNotifyException:exception]; JSValueRef result = JSObjectCallAsFunction([_context JSGlobalContextRef], object, 0, argumentCount, arguments.data(), &exception); if (exception) return [_context valueFromNotifyException:exception]; return [JSValue valueWithJSValueRef:result inContext:_context]; } - (JSValue *)constructWithArguments:(NSArray *)argumentArray { JSC::JSGlobalObject* globalObject = toJS([_context JSGlobalContextRef]); JSC::VM& vm = globalObject->vm(); JSC::JSLockHolder locker(vm); NSUInteger argumentCount = [argumentArray count]; JSC::MarkedJSValueRefArray arguments([_context JSGlobalContextRef], argumentCount); for (unsigned i = 0; i < argumentCount; ++i) arguments[i] = objectToValue(_context, [argumentArray objectAtIndex:i]); JSValueRef exception = 0; JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception); if (exception) return [_context valueFromNotifyException:exception]; JSObjectRef result = JSObjectCallAsConstructor([_context JSGlobalContextRef], object, argumentCount, arguments.data(), &exception); if (exception) return [_context valueFromNotifyException:exception]; return [JSValue valueWithJSValueRef:result inContext:_context]; } - (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments { JSC::JSGlobalObject* globalObject = toJS([_context JSGlobalContextRef]); JSC::VM& vm = globalObject->vm(); JSC::JSLockHolder locker(vm); NSUInteger argumentCount = [arguments count]; JSC::MarkedJSValueRefArray argumentArray([_context JSGlobalContextRef], argumentCount); for (unsigned i = 0; i < argumentCount; ++i) argumentArray[i] = objectToValue(_context, [arguments objectAtIndex:i]); JSValueRef exception = 0; JSObjectRef thisObject = JSValueToObject([_context JSGlobalContextRef], m_value, &exception); if (exception) return [_context valueFromNotifyException:exception]; auto name = OpaqueJSString::tryCreate(method); JSValueRef function = JSObjectGetProperty([_context JSGlobalContextRef], thisObject, name.get(), &exception); if (exception) return [_context valueFromNotifyException:exception]; JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], function, &exception); if (exception) return [_context valueFromNotifyException:exception]; JSValueRef result = JSObjectCallAsFunction([_context JSGlobalContextRef], object, thisObject, argumentCount, argumentArray.data(), &exception); if (exception) return [_context valueFromNotifyException:exception]; return [JSValue valueWithJSValueRef:result inContext:_context]; } @end @implementation JSValue(StructSupport) - (CGPoint)toPoint { return (CGPoint){ static_cast([self[@"x"] toDouble]), static_cast([self[@"y"] toDouble]) }; } - (NSRange)toRange { return (NSRange){ [[self[@"location"] toNumber] unsignedIntegerValue], [[self[@"length"] toNumber] unsignedIntegerValue] }; } - (CGRect)toRect { return (CGRect){ [self toPoint], [self toSize] }; } - (CGSize)toSize { return (CGSize){ static_cast([self[@"width"] toDouble]), static_cast([self[@"height"] toDouble]) }; } + (JSValue *)valueWithPoint:(CGPoint)point inContext:(JSContext *)context { return [JSValue valueWithObject:@{ @"x":@(point.x), @"y":@(point.y) } inContext:context]; } + (JSValue *)valueWithRange:(NSRange)range inContext:(JSContext *)context { return [JSValue valueWithObject:@{ @"location":@(range.location), @"length":@(range.length) } inContext:context]; } + (JSValue *)valueWithRect:(CGRect)rect inContext:(JSContext *)context { return [JSValue valueWithObject:@{ @"x":@(rect.origin.x), @"y":@(rect.origin.y), @"width":@(rect.size.width), @"height":@(rect.size.height) } inContext:context]; } + (JSValue *)valueWithSize:(CGSize)size inContext:(JSContext *)context { return [JSValue valueWithObject:@{ @"width":@(size.width), @"height":@(size.height) } inContext:context]; } @end @implementation JSValue(SubscriptSupport) - (JSValue *)objectForKeyedSubscript:(id)key { return [self valueForProperty:key]; } - (JSValue *)objectAtIndexedSubscript:(NSUInteger)index { return [self valueAtIndex:index]; } - (void)setObject:(id)object forKeyedSubscript:(id)key { [self setValue:object forProperty:key]; } - (void)setObject:(id)object atIndexedSubscript:(NSUInteger)index { [self setValue:object atIndex:index]; } @end inline bool isDate(JSC::VM& vm, JSObjectRef object, JSGlobalContextRef context) { JSC::JSLockHolder locker(toJS(context)); return toJS(object)->inherits(vm); } inline bool isArray(JSC::VM& vm, JSObjectRef object, JSGlobalContextRef context) { JSC::JSLockHolder locker(toJS(context)); return toJS(object)->inherits(vm); } @implementation JSValue(Internal) enum ConversionType { ContainerNone, ContainerArray, ContainerDictionary }; class JSContainerConvertor { public: struct Task { JSValueRef js; id objc; ConversionType type; }; JSContainerConvertor(JSGlobalContextRef context) : m_context(context) { } id convert(JSValueRef property); void add(Task); Task take(); bool isWorkListEmpty() const { return !m_worklist.size(); } private: JSGlobalContextRef m_context; HashMap m_objectMap; Vector m_worklist; Vector> m_jsValues; }; inline id JSContainerConvertor::convert(JSValueRef value) { auto iter = m_objectMap.find(value); if (iter != m_objectMap.end()) return iter->value; Task result = valueToObjectWithoutCopy(m_context, value); if (result.js) add(result); return result.objc; } void JSContainerConvertor::add(Task task) { JSC::JSGlobalObject* globalObject = toJS(m_context); m_jsValues.append(JSC::Strong(globalObject->vm(), toJSForGC(globalObject, task.js))); m_objectMap.add(task.js, task.objc); if (task.type != ContainerNone) m_worklist.append(task); } JSContainerConvertor::Task JSContainerConvertor::take() { ASSERT(!isWorkListEmpty()); Task last = m_worklist.last(); m_worklist.removeLast(); return last; } #if ENABLE(REMOTE_INSPECTOR) static void reportExceptionToInspector(JSGlobalContextRef context, JSC::JSValue exceptionValue) { JSC::JSGlobalObject* globalObject = toJS(context); JSC::VM& vm = globalObject->vm(); JSC::Exception* exception = JSC::Exception::create(vm, exceptionValue); globalObject->inspectorController().reportAPIException(globalObject, exception); } #endif static JSContainerConvertor::Task valueToObjectWithoutCopy(JSGlobalContextRef context, JSValueRef value) { JSC::JSGlobalObject* globalObject = toJS(context); JSC::VM& vm = globalObject->vm(); if (!JSValueIsObject(context, value)) { id primitive; if (JSValueIsBoolean(context, value)) primitive = JSValueToBoolean(context, value) ? @YES : @NO; else if (JSValueIsNumber(context, value)) { // Normalize the number, so it will unique correctly in the hash map - // it's nicer not to leak this internal implementation detail! value = JSValueMakeNumber(context, JSValueToNumber(context, value, 0)); primitive = @(JSValueToNumber(context, value, 0)); } else if (JSValueIsString(context, value)) { // Would be nice to unique strings, too. auto jsstring = adoptRef(JSValueToStringCopy(context, value, 0)); primitive = CFBridgingRelease(JSStringCopyCFString(kCFAllocatorDefault, jsstring.get())); } else if (JSValueIsNull(context, value)) primitive = [NSNull null]; else { ASSERT(JSValueIsUndefined(context, value)); primitive = nil; } return { value, primitive, ContainerNone }; } JSObjectRef object = JSValueToObject(context, value, 0); if (id wrapped = tryUnwrapObjcObject(context, object)) return { object, wrapped, ContainerNone }; if (isDate(vm, object, context)) return { object, [NSDate dateWithTimeIntervalSince1970:JSValueToNumber(context, object, 0) / 1000.0], ContainerNone }; if (isArray(vm, object, context)) return { object, [NSMutableArray array], ContainerArray }; return { object, [NSMutableDictionary dictionary], ContainerDictionary }; } static id containerValueToObject(JSGlobalContextRef context, JSContainerConvertor::Task task) { ASSERT(task.type != ContainerNone); JSC::JSLockHolder locker(toJS(context)); JSContainerConvertor convertor(context); convertor.add(task); ASSERT(!convertor.isWorkListEmpty()); do { JSContainerConvertor::Task current = convertor.take(); ASSERT(JSValueIsObject(context, current.js)); JSObjectRef js = JSValueToObject(context, current.js, 0); if (current.type == ContainerArray) { ASSERT([current.objc isKindOfClass:[NSMutableArray class]]); NSMutableArray *array = (NSMutableArray *)current.objc; auto lengthString = OpaqueJSString::tryCreate("length"_s); unsigned length = JSC::toUInt32(JSValueToNumber(context, JSObjectGetProperty(context, js, lengthString.get(), 0), 0)); for (unsigned i = 0; i < length; ++i) { id objc = convertor.convert(JSObjectGetPropertyAtIndex(context, js, i, 0)); [array addObject:objc ? objc : [NSNull null]]; } } else { ASSERT([current.objc isKindOfClass:[NSMutableDictionary class]]); NSMutableDictionary *dictionary = (NSMutableDictionary *)current.objc; JSC::JSLockHolder locker(toJS(context)); JSPropertyNameArrayRef propertyNameArray = JSObjectCopyPropertyNames(context, js); size_t length = JSPropertyNameArrayGetCount(propertyNameArray); for (size_t i = 0; i < length; ++i) { JSStringRef propertyName = JSPropertyNameArrayGetNameAtIndex(propertyNameArray, i); if (id objc = convertor.convert(JSObjectGetProperty(context, js, propertyName, 0))) dictionary[(__bridge NSString *)adoptCF(JSStringCopyCFString(kCFAllocatorDefault, propertyName)).get()] = objc; } JSPropertyNameArrayRelease(propertyNameArray); } } while (!convertor.isWorkListEmpty()); return task.objc; } id valueToObject(JSContext *context, JSValueRef value) { JSContainerConvertor::Task result = valueToObjectWithoutCopy([context JSGlobalContextRef], value); if (result.type == ContainerNone) return result.objc; return containerValueToObject([context JSGlobalContextRef], result); } id valueToNumber(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception) { ASSERT(!*exception); if (id wrapped = tryUnwrapObjcObject(context, value)) { if ([wrapped isKindOfClass:[NSNumber class]]) return wrapped; } if (JSValueIsBoolean(context, value)) return JSValueToBoolean(context, value) ? @YES : @NO; double result = JSValueToNumber(context, value, exception); return @(*exception ? std::numeric_limits::quiet_NaN() : result); } id valueToString(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception) { ASSERT(!*exception); if (id wrapped = tryUnwrapObjcObject(context, value)) { if ([wrapped isKindOfClass:[NSString class]]) return wrapped; } auto jsstring = adoptRef(JSValueToStringCopy(context, value, exception)); if (*exception) { ASSERT(!jsstring); return nil; } return CFBridgingRelease(JSStringCopyCFString(kCFAllocatorDefault, jsstring.get())); } id valueToDate(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception) { ASSERT(!*exception); if (id wrapped = tryUnwrapObjcObject(context, value)) { if ([wrapped isKindOfClass:[NSDate class]]) return wrapped; } double result = JSValueToNumber(context, value, exception) / 1000.0; return *exception ? nil : [NSDate dateWithTimeIntervalSince1970:result]; } id valueToArray(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception) { ASSERT(!*exception); if (id wrapped = tryUnwrapObjcObject(context, value)) { if ([wrapped isKindOfClass:[NSArray class]]) return wrapped; } if (JSValueIsObject(context, value)) return containerValueToObject(context, { value, [NSMutableArray array], ContainerArray}); JSC::JSLockHolder locker(toJS(context)); if (!(JSValueIsNull(context, value) || JSValueIsUndefined(context, value))) { JSC::JSObject* exceptionObject = JSC::createTypeError(toJS(context), "Cannot convert primitive to NSArray"_s); *exception = toRef(exceptionObject); #if ENABLE(REMOTE_INSPECTOR) reportExceptionToInspector(context, exceptionObject); #endif } return nil; } id valueToDictionary(JSGlobalContextRef context, JSValueRef value, JSValueRef* exception) { ASSERT(!*exception); if (id wrapped = tryUnwrapObjcObject(context, value)) { if ([wrapped isKindOfClass:[NSDictionary class]]) return wrapped; } if (JSValueIsObject(context, value)) return containerValueToObject(context, { value, [NSMutableDictionary dictionary], ContainerDictionary}); JSC::JSLockHolder locker(toJS(context)); if (!(JSValueIsNull(context, value) || JSValueIsUndefined(context, value))) { JSC::JSObject* exceptionObject = JSC::createTypeError(toJS(context), "Cannot convert primitive to NSDictionary"_s); *exception = toRef(exceptionObject); #if ENABLE(REMOTE_INSPECTOR) reportExceptionToInspector(context, exceptionObject); #endif } return nil; } class ObjcContainerConvertor { public: struct Task { id objc; JSValueRef js; ConversionType type; }; ObjcContainerConvertor(JSContext *context) : m_context(context) { } JSValueRef convert(id object); void add(Task); Task take(); bool isWorkListEmpty() const { return !m_worklist.size(); } private: JSContext *m_context; HashMap<__unsafe_unretained id, JSValueRef> m_objectMap; Vector m_worklist; Vector> m_jsValues; }; JSValueRef ObjcContainerConvertor::convert(id object) { ASSERT(object); auto it = m_objectMap.find(object); if (it != m_objectMap.end()) return it->value; ObjcContainerConvertor::Task task = objectToValueWithoutCopy(m_context, object); add(task); return task.js; } void ObjcContainerConvertor::add(ObjcContainerConvertor::Task task) { JSC::JSGlobalObject* globalObject = toJS(m_context.JSGlobalContextRef); m_jsValues.append(JSC::Strong(globalObject->vm(), toJSForGC(globalObject, task.js))); m_objectMap.add(task.objc, task.js); if (task.type != ContainerNone) m_worklist.append(task); } ObjcContainerConvertor::Task ObjcContainerConvertor::take() { ASSERT(!isWorkListEmpty()); Task last = m_worklist.last(); m_worklist.removeLast(); return last; } inline bool isNSBoolean(id object) { ASSERT([@YES class] == [@NO class]); ASSERT([@YES class] != [NSNumber class]); ASSERT([[@YES class] isSubclassOfClass:[NSNumber class]]); return [object isKindOfClass:[@YES class]]; } static ObjcContainerConvertor::Task objectToValueWithoutCopy(JSContext *context, id object) { JSGlobalContextRef contextRef = [context JSGlobalContextRef]; if (!object) return { object, JSValueMakeUndefined(contextRef), ContainerNone }; if (!class_conformsToProtocol(object_getClass(object), getJSExportProtocol())) { if ([object isKindOfClass:[NSArray class]]) return { object, JSObjectMakeArray(contextRef, 0, NULL, 0), ContainerArray }; if ([object isKindOfClass:[NSDictionary class]]) return { object, JSObjectMake(contextRef, 0, 0), ContainerDictionary }; if ([object isKindOfClass:[NSNull class]]) return { object, JSValueMakeNull(contextRef), ContainerNone }; if ([object isKindOfClass:[JSValue class]]) return { object, ((JSValue *)object)->m_value, ContainerNone }; if ([object isKindOfClass:[NSString class]]) { auto string = OpaqueJSString::tryCreate((NSString *)object); return { object, JSValueMakeString(contextRef, string.get()), ContainerNone }; } if ([object isKindOfClass:[NSNumber class]]) { if (isNSBoolean(object)) return { object, JSValueMakeBoolean(contextRef, [object boolValue]), ContainerNone }; return { object, JSValueMakeNumber(contextRef, [object doubleValue]), ContainerNone }; } if ([object isKindOfClass:[NSDate class]]) { JSValueRef argument = JSValueMakeNumber(contextRef, [object timeIntervalSince1970] * 1000.0); JSObjectRef result = JSObjectMakeDate(contextRef, 1, &argument, 0); return { object, result, ContainerNone }; } if ([object isKindOfClass:[JSManagedValue class]]) { JSValue *value = [static_cast(object) value]; if (!value) return { object, JSValueMakeUndefined(contextRef), ContainerNone }; return { object, value->m_value, ContainerNone }; } } return { object, valueInternalValue([context wrapperForObjCObject:object]), ContainerNone }; } JSValueRef objectToValue(JSContext *context, id object) { JSGlobalContextRef contextRef = [context JSGlobalContextRef]; ObjcContainerConvertor::Task task = objectToValueWithoutCopy(context, object); if (task.type == ContainerNone) return task.js; JSC::JSLockHolder locker(toJS(contextRef)); ObjcContainerConvertor convertor(context); convertor.add(task); ASSERT(!convertor.isWorkListEmpty()); do { ObjcContainerConvertor::Task current = convertor.take(); ASSERT(JSValueIsObject(contextRef, current.js)); JSObjectRef js = JSValueToObject(contextRef, current.js, 0); if (current.type == ContainerArray) { ASSERT([current.objc isKindOfClass:[NSArray class]]); NSArray *array = (NSArray *)current.objc; NSUInteger count = [array count]; for (NSUInteger index = 0; index < count; ++index) JSObjectSetPropertyAtIndex(contextRef, js, index, convertor.convert([array objectAtIndex:index]), 0); } else { ASSERT(current.type == ContainerDictionary); ASSERT([current.objc isKindOfClass:[NSDictionary class]]); NSDictionary *dictionary = (NSDictionary *)current.objc; for (id key in [dictionary keyEnumerator]) { if ([key isKindOfClass:[NSString class]]) { auto propertyName = OpaqueJSString::tryCreate((NSString *)key); JSObjectSetProperty(contextRef, js, propertyName.get(), convertor.convert([dictionary objectForKey:key]), 0, 0); } } } } while (!convertor.isWorkListEmpty()); return task.js; } JSValueRef valueInternalValue(JSValue * value) { return value->m_value; } + (JSValue *)valueWithJSValueRef:(JSValueRef)value inContext:(JSContext *)context { return [context wrapperForJSObject:value]; } - (JSValue *)init { return nil; } - (JSValue *)initWithValue:(JSValueRef)value inContext:(JSContext *)context { if (!value || !context) return nil; self = [super init]; if (!self) return nil; _context = [context retain]; m_value = value; JSValueProtect([_context JSGlobalContextRef], m_value); return self; } struct StructTagHandler { SEL typeToValueSEL; SEL valueToTypeSEL; }; typedef HashMap StructHandlers; static StructHandlers* createStructHandlerMap() { StructHandlers* structHandlers = new StructHandlers(); size_t valueWithXinContextLength = strlen("valueWithX:inContext:"); size_t toXLength = strlen("toX"); // Step 1: find all valueWith:inContext: class methods in JSValue. forEachMethodInClass(object_getClass([JSValue class]), ^(Method method){ SEL selector = method_getName(method); const char* name = sel_getName(selector); size_t nameLength = strlen(name); // Check for valueWith:context: if (nameLength < valueWithXinContextLength || memcmp(name, "valueWith", 9) || memcmp(name + nameLength - 11, ":inContext:", 11)) return; // Check for [ id, SEL, , ] if (method_getNumberOfArguments(method) != 4) return; char idType[3]; // Check 2nd argument type is "@" { auto secondType = adoptSystem(method_copyArgumentType(method, 3)); if (strcmp(secondType.get(), "@") != 0) return; } // Check result type is also "@" method_getReturnType(method, idType, 3); if (strcmp(idType, "@") != 0) return; { auto type = adoptSystem(method_copyArgumentType(method, 2)); structHandlers->add(StringImpl::create(type.get()), (StructTagHandler) { selector, 0 }); } }); // Step 2: find all to instance methods in JSValue. forEachMethodInClass([JSValue class], ^(Method method){ SEL selector = method_getName(method); const char* name = sel_getName(selector); size_t nameLength = strlen(name); // Check for to if (nameLength < toXLength || memcmp(name, "to", 2)) return; // Check for [ id, SEL ] if (method_getNumberOfArguments(method) != 2) return; // Try to find a matching valueWith:context: method. auto type = adoptSystem(method_copyReturnType(method)); StructHandlers::iterator iter = structHandlers->find(type.get()); if (iter == structHandlers->end()) return; StructTagHandler& handler = iter->value; // check that strlen() == strlen() const char* valueWithName = sel_getName(handler.typeToValueSEL); size_t valueWithLength = strlen(valueWithName); if (valueWithLength - valueWithXinContextLength != nameLength - toXLength) return; // Check that == if (memcmp(valueWithName + 9, name + 2, nameLength - toXLength - 1)) return; handler.valueToTypeSEL = selector; }); // Step 3: clean up - remove entries where we found prospective valueWith:inContext: conversions, but no matching to methods. typedef HashSet RemoveSet; RemoveSet removeSet; for (StructHandlers::iterator iter = structHandlers->begin(); iter != structHandlers->end(); ++iter) { StructTagHandler& handler = iter->value; if (!handler.valueToTypeSEL) removeSet.add(iter->key); } for (RemoveSet::iterator iter = removeSet.begin(); iter != removeSet.end(); ++iter) structHandlers->remove(*iter); return structHandlers; } static StructTagHandler* handerForStructTag(const char* encodedType) { static Lock handerForStructTagLock; LockHolder lockHolder(&handerForStructTagLock); static StructHandlers* structHandlers = createStructHandlerMap(); StructHandlers::iterator iter = structHandlers->find(encodedType); if (iter == structHandlers->end()) return 0; return &iter->value; } + (SEL)selectorForStructToValue:(const char *)structTag { StructTagHandler* handler = handerForStructTag(structTag); return handler ? handler->typeToValueSEL : nil; } + (SEL)selectorForValueToStruct:(const char *)structTag { StructTagHandler* handler = handerForStructTag(structTag); return handler ? handler->valueToTypeSEL : nil; } NSInvocation *typeToValueInvocationFor(const char* encodedType) { SEL selector = [JSValue selectorForStructToValue:encodedType]; if (!selector) return 0; const char* methodTypes = method_getTypeEncoding(class_getClassMethod([JSValue class], selector)); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:methodTypes]]; [invocation setSelector:selector]; return invocation; } NSInvocation *valueToTypeInvocationFor(const char* encodedType) { SEL selector = [JSValue selectorForValueToStruct:encodedType]; if (!selector) return 0; const char* methodTypes = method_getTypeEncoding(class_getInstanceMethod([JSValue class], selector)); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:methodTypes]]; [invocation setSelector:selector]; return invocation; } @end #endif