/* * Copyright (C) 2013 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. */ #include "config.h" #import "JavaScriptCore.h" #if JS_OBJC_API_ENABLED #import "APICast.h" #import "APIShims.h" #import "Error.h" #import "JSBlockAdaptor.h" #import "JSContextInternal.h" #import "JSWrapperMap.h" #import "JSValueInternal.h" #import "ObjCCallbackFunction.h" #import "ObjcRuntimeExtras.h" #import #import class CallbackArgument { public: virtual ~CallbackArgument(); virtual void set(NSInvocation *, NSInteger, JSContext *, JSValueRef, JSValueRef*) = 0; OwnPtr m_next; }; CallbackArgument::~CallbackArgument() { } class CallbackArgumentBoolean : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef*) override { bool value = JSValueToBoolean(contextInternalContext(context), argument); [invocation setArgument:&value atIndex:argumentNumber]; } }; template class CallbackArgumentInteger : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { T value = (T)JSC::toInt32(JSValueToNumber(contextInternalContext(context), argument, exception)); [invocation setArgument:&value atIndex:argumentNumber]; } }; template class CallbackArgumentDouble : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { T value = (T)JSValueToNumber(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentJSValue : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef*) override { JSValue *value = [JSValue valueWithValue:argument inContext:context]; [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentId : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef*) override { id value = valueToObject(context, argument); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentOfClass : public CallbackArgument { public: CallbackArgumentOfClass(Class cls) : CallbackArgument() , m_class(cls) { [m_class retain]; } private: virtual ~CallbackArgumentOfClass() { [m_class release]; } virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { JSGlobalContextRef contextRef = contextInternalContext(context); id object = tryUnwrapObjcObject(contextRef, argument); if (object && [object isKindOfClass:m_class]) { [invocation setArgument:&object atIndex:argumentNumber]; return; } if (JSValueIsNull(contextRef, argument) || JSValueIsUndefined(contextRef, argument)) { object = nil; [invocation setArgument:&object atIndex:argumentNumber]; return; } *exception = toRef(JSC::createTypeError(toJS(contextRef), "Argument does not match Objective-C Class")); } Class m_class; }; class CallbackArgumentNSNumber : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id value = valueToNumber(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentNSString : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id value = valueToString(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentNSDate : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id value = valueToDate(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentNSArray : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id value = valueToArray(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentNSDictionary : public CallbackArgument { virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id value = valueToDictionary(contextInternalContext(context), argument, exception); [invocation setArgument:&value atIndex:argumentNumber]; } }; class CallbackArgumentStruct : public CallbackArgument { public: CallbackArgumentStruct(NSInvocation *conversionInvocation, const char* encodedType) : m_conversionInvocation(conversionInvocation) , m_buffer(encodedType) { } private: virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef*) override { JSValue *value = [JSValue valueWithValue:argument inContext:context]; [m_conversionInvocation invokeWithTarget:value]; [m_conversionInvocation getReturnValue:m_buffer]; [invocation setArgument:m_buffer atIndex:argumentNumber]; } RetainPtr m_conversionInvocation; StructBuffer m_buffer; }; class CallbackArgumentBlockCallback : public CallbackArgument { public: static CallbackArgumentBlockCallback* createAdoptingJSBlockAdaptor(JSBlockAdaptor *adaptor) { return new CallbackArgumentBlockCallback(adaptor); } private: CallbackArgumentBlockCallback(JSBlockAdaptor *adaptor) : m_adaptor(adaptor) { } virtual ~CallbackArgumentBlockCallback() { [m_adaptor release]; } virtual void set(NSInvocation *invocation, NSInteger argumentNumber, JSContext *context, JSValueRef argument, JSValueRef* exception) override { id block = [m_adaptor blockFromValue:argument inContext:context withException:exception]; [invocation setArgument:&block atIndex:argumentNumber]; } JSBlockAdaptor *m_adaptor; }; class ArgumentTypeDelegate { public: typedef CallbackArgument* ResultType; template static ResultType typeInteger() { return new CallbackArgumentInteger; } template static ResultType typeDouble() { return new CallbackArgumentDouble; } static ResultType typeBool() { return new CallbackArgumentBoolean; } static ResultType typeVoid() { RELEASE_ASSERT_NOT_REACHED(); return 0; } static ResultType typeId() { return new CallbackArgumentId; } static ResultType typeOfClass(const char* begin, const char* end) { StringRange copy(begin, end); Class cls = objc_getClass(copy); if (!cls) return 0; if (cls == [JSValue class]) return new CallbackArgumentJSValue; if (cls == [NSString class]) return new CallbackArgumentNSString; if (cls == [NSNumber class]) return new CallbackArgumentNSNumber; if (cls == [NSDate class]) return new CallbackArgumentNSDate; if (cls == [NSArray class]) return new CallbackArgumentNSArray; if (cls == [NSDictionary class]) return new CallbackArgumentNSDictionary; return new CallbackArgumentOfClass(cls); } static ResultType typeBlock(const char* begin, const char* end) { StringRange copy(begin, end); return CallbackArgumentBlockCallback::createAdoptingJSBlockAdaptor([[JSBlockAdaptor alloc] initWithBlockSignatureFromProtocol:copy]); } static ResultType typeStruct(const char* begin, const char* end) { StringRange copy(begin, end); if (NSInvocation *invocation = valueToTypeInvocationFor(copy)) return new CallbackArgumentStruct(invocation, copy); return 0; } }; class CallbackResult { public: virtual ~CallbackResult() { } virtual JSValueRef get(NSInvocation *, JSContext *, JSValueRef*) = 0; }; class CallbackResultVoid : public CallbackResult { virtual JSValueRef get(NSInvocation *, JSContext *context, JSValueRef*) override { return JSValueMakeUndefined(contextInternalContext(context)); } }; class CallbackResultId : public CallbackResult { virtual JSValueRef get(NSInvocation *invocation, JSContext *context, JSValueRef*) override { id value; [invocation getReturnValue:&value]; return objectToValue(context, value); } }; template class CallbackResultNumeric : public CallbackResult { virtual JSValueRef get(NSInvocation *invocation, JSContext *context, JSValueRef*) override { T value; [invocation getReturnValue:&value]; return JSValueMakeNumber(contextInternalContext(context), value); } }; class CallbackResultBoolean : public CallbackResult { virtual JSValueRef get(NSInvocation *invocation, JSContext *context, JSValueRef*) override { bool value; [invocation getReturnValue:&value]; return JSValueMakeBoolean(contextInternalContext(context), value); } }; class CallbackResultStruct : public CallbackResult { public: CallbackResultStruct(NSInvocation *conversionInvocation, const char* encodedType) : m_conversionInvocation(conversionInvocation) , m_buffer(encodedType) { } private: virtual JSValueRef get(NSInvocation *invocation, JSContext *context, JSValueRef*) override { [invocation getReturnValue:m_buffer]; [m_conversionInvocation setArgument:m_buffer atIndex:2]; [m_conversionInvocation setArgument:&context atIndex:3]; [m_conversionInvocation invokeWithTarget:[JSValue class]]; JSValue *value; [m_conversionInvocation getReturnValue:&value]; return valueInternalValue(value); } RetainPtr m_conversionInvocation; StructBuffer m_buffer; }; class ResultTypeDelegate { public: typedef CallbackResult* ResultType; template static ResultType typeInteger() { return new CallbackResultNumeric; } template static ResultType typeDouble() { return new CallbackResultNumeric; } static ResultType typeBool() { return new CallbackResultBoolean; } static ResultType typeVoid() { return new CallbackResultVoid; } static ResultType typeId() { return new CallbackResultId(); } static ResultType typeOfClass(const char*, const char*) { return new CallbackResultId(); } static ResultType typeBlock(const char*, const char*) { return new CallbackResultId(); } static ResultType typeStruct(const char* begin, const char* end) { StringRange copy(begin, end); if (NSInvocation *invocation = typeToValueInvocationFor(copy)) return new CallbackResultStruct(invocation, copy); return 0; } }; enum CallbackType { CallbackInstanceMethod, CallbackClassMethod, CallbackBlock }; class ObjCCallbackFunction { public: ObjCCallbackFunction(JSContext *context, NSInvocation *invocation, CallbackType type, Class instanceClass, PassOwnPtr arguments, PassOwnPtr result) : m_context(context) , m_type(type) , m_instanceClass([instanceClass retain]) , m_invocation(invocation) , m_arguments(arguments) , m_result(result) { ASSERT(type != CallbackInstanceMethod || instanceClass); } ~ObjCCallbackFunction() { [m_instanceClass release]; } JSValueRef call(JSContext *context, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); JSContext *context() { return m_context.get(); } id wrappedBlock() { return m_type == CallbackBlock ? [m_invocation target] : nil; } private: WeakContextRef m_context; CallbackType m_type; Class m_instanceClass; RetainPtr m_invocation; OwnPtr m_arguments; OwnPtr m_result; }; static void objCCallbackFunctionFinalize(JSObjectRef object) { delete static_cast(JSObjectGetPrivate(object)); } static JSValueRef objCCallbackFunctionCallAsFunction(JSContextRef callerContext, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { // Retake the API lock - we need this for a few reasons: // (1) We don't want to support the C-API's confusing drops-locks-once policy - should only drop locks if we can do so recursively. // (2) We're calling some JSC internals that require us to be on the 'inside' - e.g. createTypeError. // (3) We need to be locked (per context would be fine) against conflicting usage of the ObjCCallbackFunction's NSInvocation. JSC::APIEntryShim entryShim(toJS(callerContext)); ObjCCallbackFunction* callback = static_cast(JSObjectGetPrivate(function)); JSContext *context = callback->context(); if (!context) { // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105894 // Rather than requiring that the context be retained, it would probably be more // appropriate to use a new JSContext instance (creating one if necessary). *exception = toRef(JSC::createTypeError(toJS(callerContext), "Objective-C callback function context released")); return JSValueMakeUndefined(callerContext); } CallbackData callbackData; JSValueRef result; @autoreleasepool { [context beginCallbackWithData:&callbackData thisValue:thisObject argumentCount:argumentCount arguments:arguments]; result = callback->call(context, thisObject, argumentCount, arguments, exception); if (context.exception) *exception = valueInternalValue(context.exception); [context endCallbackWithData:&callbackData]; } return result; } static JSClassRef createObjCCallbackFunctionClass() { JSClassDefinition definition; definition = kJSClassDefinitionEmpty; definition.className = "Function"; definition.finalize = objCCallbackFunctionFinalize; definition.callAsFunction = objCCallbackFunctionCallAsFunction; return JSClassCreate(&definition); } static JSClassRef objCCallbackFunctionClass() { static SpinLock initLock = SPINLOCK_INITIALIZER; SpinLockHolder lockHolder(&initLock); static JSClassRef classRef = createObjCCallbackFunctionClass(); return classRef; } JSValueRef ObjCCallbackFunction::call(JSContext *context, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { JSGlobalContextRef contextRef = contextInternalContext(context); size_t firstArgument; switch (m_type) { case CallbackInstanceMethod: { id target = tryUnwrapObjcObject(contextRef, thisObject); if (!target || ![target isKindOfClass:m_instanceClass]) { *exception = toRef(JSC::createTypeError(toJS(contextRef), "self type check failed for Objective-C instance method")); return JSValueMakeUndefined(contextRef); } [m_invocation setTarget:target]; } // fallthrough - firstArgument for CallbackInstanceMethod is also 2! case CallbackClassMethod: firstArgument = 2; break; case CallbackBlock: firstArgument = 1; } size_t argumentNumber = 0; for (CallbackArgument* argument = m_arguments.get(); argument; argument = argument->m_next.get()) { JSValueRef value = argumentNumber < argumentCount ? arguments[argumentNumber] : JSValueMakeUndefined(contextRef); argument->set(m_invocation.get(), argumentNumber + firstArgument, context, value, exception); if (*exception) return JSValueMakeUndefined(contextRef); ++argumentNumber; } [m_invocation invoke]; return m_result->get(m_invocation.get(), context, exception); } static bool blockSignatureContainsClass() { static bool containsClass = ^{ id block = ^(NSString *string){ return string; }; return _Block_has_signature(block) && strstr(_Block_signature(block), "NSString"); }(); return containsClass; } inline bool skipNumber(const char*& position) { if (!isASCIIDigit(*position)) return false; while (isASCIIDigit(*++position)) { } return true; } static JSObjectRef objCCallbackFunctionForInvocation(JSContext *context, NSInvocation *invocation, CallbackType type, Class instanceClass, const char* signatureWithObjcClasses) { const char* position = signatureWithObjcClasses; OwnPtr result = adoptPtr(parseObjCType(position)); if (!result || !skipNumber(position)) return nil; switch (type) { case CallbackInstanceMethod: case CallbackClassMethod: // Methods are passed two implicit arguments - (id)self, and the selector. if ('@' != *position++ || !skipNumber(position) || ':' != *position++ || !skipNumber(position)) return nil; break; case CallbackBlock: // Blocks are passed one implicit argument - the block, of type "@?". if (('@' != *position++) || ('?' != *position++) || !skipNumber(position)) return nil; // Only allow arguments of type 'id' if the block signature contains the NS type information. if ((!blockSignatureContainsClass() && strchr(position, '@'))) return nil; break; } OwnPtr arguments = 0; OwnPtr* nextArgument = &arguments; unsigned argumentCount = 0; while (*position) { OwnPtr argument = adoptPtr(parseObjCType(position)); if (!argument || !skipNumber(position)) return nil; *nextArgument = argument.release(); nextArgument = &(*nextArgument)->m_next; ++argumentCount; } // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105892 // The result should be a regular host function, rather than a callable C API object. // Patch in the right length & [Prototype] values for now, but should fix this. // Function.prototype.toString currently fails, since this is not yet a subclass of functon, but call & apply do work. JSObjectRef functionObject = JSObjectMake(contextInternalContext(context), objCCallbackFunctionClass(), new ObjCCallbackFunction(context, invocation, type, instanceClass, arguments.release(), result.release())); JSValue *value = [JSValue valueWithValue:functionObject inContext:context]; value[@"length"] = @(argumentCount); value[@"__proto__"] = context[@"Function"][@"prototype"]; value[@"toString"] = [context evaluateScript:@"(function(){ return '" "function () {" "\\n" " [native code]" "\\n" "}" "\\n" "'})"]; return functionObject; } JSObjectRef objCCallbackFunctionForMethod(JSContext *context, Class cls, Protocol *protocol, BOOL isInstanceMethod, SEL sel, const char* types) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:types]]; [invocation setSelector:sel]; if (!isInstanceMethod) [invocation setTarget:cls]; return objCCallbackFunctionForInvocation(context, invocation, isInstanceMethod ? CallbackInstanceMethod : CallbackClassMethod, isInstanceMethod ? cls : nil, _protocol_getMethodTypeEncoding(protocol, sel, YES, isInstanceMethod)); } JSObjectRef objCCallbackFunctionForBlock(JSContext *context, id target) { if (!_Block_has_signature(target)) return 0; const char* signature = _Block_signature(target); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[NSMethodSignature signatureWithObjCTypes:signature]]; [invocation setTarget:target]; return objCCallbackFunctionForInvocation(context, invocation, CallbackBlock, nil, signature); } id tryUnwrapBlock(JSGlobalContextRef context, JSObjectRef object) { if (!JSValueIsObjectOfClass(context, object, objCCallbackFunctionClass())) return nil; return (static_cast(JSObjectGetPrivate(object)))->wrappedBlock(); } #endif