darling-JavaScriptCore/API/JSValue.mm
Thomas A 1151b227f3 Workaround an issue where variables must be defined in the interface when building for i386
I'm honestly surprised that this issue doesn't appear with the older version of JavaScriptCore...
2023-04-18 21:20:25 -07:00

1265 lines
42 KiB
Plaintext

/*
* 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 <wtf/Expected.h>
#import <wtf/HashMap.h>
#import <wtf/HashSet.h>
#import <wtf/Lock.h>
#import <wtf/Vector.h>
#import <wtf/text/WTFString.h>
#import <wtf/text/StringHash.h>
#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<double>::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<typename Result, typename NSStringFunction, typename JSValueFunction, typename... Types>
inline Expected<Result, JSValueRef> 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<JSValueRef>(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, JSValueRef>(result);
}
- (JSValue *)valueForProperty:(id)key
{
auto result = performPropertyOperation<JSValueRef>(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<Unit>(stringSetProperty, jsValueSetProperty, self, key, objectToValue(_context, value), kJSPropertyAttributeNone);
if (!result) {
[_context notifyException:result.error()];
return;
}
}
- (BOOL)deleteProperty:(JSValueProperty)key
{
Expected<BOOL, JSValueRef> result = performPropertyOperation<BOOL>(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<BOOL, JSValueRef> result = performPropertyOperation<BOOL>(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<CGFloat>([self[@"x"] toDouble]),
static_cast<CGFloat>([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<CGFloat>([self[@"width"] toDouble]),
static_cast<CGFloat>([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<JSC::DateInstance>(vm);
}
inline bool isArray(JSC::VM& vm, JSObjectRef object, JSGlobalContextRef context)
{
JSC::JSLockHolder locker(toJS(context));
return toJS(object)->inherits<JSC::JSArray>(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<JSValueRef, __unsafe_unretained id> m_objectMap;
Vector<Task> m_worklist;
Vector<JSC::Strong<JSC::Unknown>> 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<JSC::Unknown>(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<double>::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<Task> m_worklist;
Vector<JSC::Strong<JSC::Unknown>> 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<JSC::Unknown>(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<JSManagedValue *>(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<String, StructTagHandler> StructHandlers;
static StructHandlers* createStructHandlerMap()
{
StructHandlers* structHandlers = new StructHandlers();
size_t valueWithXinContextLength = strlen("valueWithX:inContext:");
size_t toXLength = strlen("toX");
// Step 1: find all valueWith<Foo>: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<Foo>:context:
if (nameLength < valueWithXinContextLength || memcmp(name, "valueWith", 9) || memcmp(name + nameLength - 11, ":inContext:", 11))
return;
// Check for [ id, SEL, <type>, <contextType> ]
if (method_getNumberOfArguments(method) != 4)
return;
char idType[3];
// Check 2nd argument type is "@"
{
auto secondType = adoptSystem<char[]>(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<char[]>(method_copyArgumentType(method, 2));
structHandlers->add(StringImpl::create(type.get()), (StructTagHandler) { selector, 0 });
}
});
// Step 2: find all to<Foo> 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<Foo>
if (nameLength < toXLength || memcmp(name, "to", 2))
return;
// Check for [ id, SEL ]
if (method_getNumberOfArguments(method) != 2)
return;
// Try to find a matching valueWith<Foo>:context: method.
auto type = adoptSystem<char[]>(method_copyReturnType(method));
StructHandlers::iterator iter = structHandlers->find(type.get());
if (iter == structHandlers->end())
return;
StructTagHandler& handler = iter->value;
// check that strlen(<foo>) == strlen(<Foo>)
const char* valueWithName = sel_getName(handler.typeToValueSEL);
size_t valueWithLength = strlen(valueWithName);
if (valueWithLength - valueWithXinContextLength != nameLength - toXLength)
return;
// Check that <Foo> == <Foo>
if (memcmp(valueWithName + 9, name + 2, nameLength - toXLength - 1))
return;
handler.valueToTypeSEL = selector;
});
// Step 3: clean up - remove entries where we found prospective valueWith<Foo>:inContext: conversions, but no matching to<Foo> methods.
typedef HashSet<String> 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