// // JSCocoa.m // JSCocoa // // Created by Patrick Geiller on 09/07/08. // Copyright 2008 __MyCompanyName__. All rights reserved. // #import "JSCocoaController.h" #import "JSCocoaLib.h" #pragma mark JS objects forward definitions // Global object static JSValueRef OSXObject_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); static void OSXObject_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); // Private JS object callbacks static void jsCocoaObject_initialize(JSContextRef, JSObjectRef); static void jsCocoaObject_finalize(JSObjectRef); static JSValueRef jsCocoaObject_callAsFunction(JSContextRef, JSObjectRef, JSObjectRef, size_t, const JSValueRef [], JSValueRef*); //static bool jsCocoaObject_hasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); static JSValueRef jsCocoaObject_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); static bool jsCocoaObject_setProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef, JSValueRef*); static bool jsCocoaObject_deleteProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); static void jsCocoaObject_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); static JSObjectRef jsCocoaObject_callAsConstructor(JSContextRef, JSObjectRef, size_t, const JSValueRef [], JSValueRef*); static JSValueRef jsCocoaObject_convertToType(JSContextRef ctx, JSObjectRef object, JSType type, JSValueRef* exception); static bool jsCocoaObject_hasInstance(JSContextRef ctx, JSObjectRef constructor, JSValueRef possibleInstance, JSValueRef* exception); static JSValueRef jsCocoaInfo_getProperty(JSContextRef, JSObjectRef, JSStringRef, JSValueRef*); static void jsCocoaInfo_getPropertyNames(JSContextRef, JSObjectRef, JSPropertyNameAccumulatorRef); // Set on valueOf callback property of objects #define JSCocoaInternalAttribute kJSPropertyAttributeDontEnum // These will be destroyed when the last JSCocoa instance dies static JSClassRef OSXObjectClass = NULL; static JSClassRef jsCocoaObjectClass = NULL; static JSClassRef jsCocoaFunctionClass= NULL; static JSClassRef jsCocoaInfoClass = NULL; static JSClassRef hashObjectClass = NULL; // Convenience method to throw a Javascript exception static void throwException(JSContextRef ctx, JSValueRef* exception, NSString* reason); BOOL isUsingStret(id argumentEncodings); JSValueRef valueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx); void* getObjCCallAddress(id argumentEncodings); JSValueRef boxedValueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx); // iPhone specifics #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE const JSClassDefinition kJSClassDefinitionEmpty = { 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }; #import "GDataDefines.h" #import "GDataXMLNode.h" #endif // Appended to swizzled method names #define OriginalMethodPrefix @"original" // // JSCocoaController // #pragma mark - #pragma mark JSCocoaController @interface JSCocoaController (Private) - (void) callDelegateForException:(JSValueRef)exception; @end @implementation JSCocoaController // Instance properties @synthesize delegate=_delegate; @synthesize useSafeDealloc, useSplitCall, useJSLint, useAutoCall, callSelectorsMissingTrailingSemicolon, canSetOnBoxedObjects, logAllExceptions; // Shared data // Given a jsFunction, retrieve its closure (jsFunction's pointer address is used as key) static id closureHash; // Given a jsFunction, retrieve its selector static id jsFunctionSelectors; // Given a jsFunction, retrieve which class it's attached to static id jsFunctionClasses; // Given a class, return the parent class implementing JSCocoaHolder method static id jsClassParents; // List of all ObjC classes written in Javascript static id jsClasses; // Given a class + methodName, retrieve its jsFunction static id jsFunctionHash; // Split call cache static id splitCallCache; // Shared instance stats static id sharedInstanceStats = nil; // Boxed objects // static id boxedObjects; // Auto call zero arg methods : allow NSWorkspace.sharedWorkspace instead of NSWorkspace.sharedWorkspace() // static BOOL useAutoCall; // Allow calling obj.method(...) instead of obj.method_(...) // static BOOL callSelectorsMissingTrailingSemicolon; // Allows setting javascript values on boxed objects (which are collected after nulling all references to them) // static BOOL canSetOnBoxedObjects; // If true, all exceptions will be sent to NSLog, event if they're caught later on by some Javascript core // static BOOL logAllExceptions; // Is speaking when throwing exceptions // static BOOL isSpeaking; // Controller count static int controllerCount = 0; // Hash used to quickly check for variadic methods, Original, Super, toString, valueOf ... NSMutableDictionary* customCallPaths; BOOL customCallPathsCacheIsClean; // Javascript functions defined for ObjC classes are stored in this hash // __globalJSFunctionRepository__[className][propertyName] // // Init // - (id)initWithGlobalContext:(JSGlobalContextRef)_ctx { // NSLog(@"JSCocoa : %p spawning with context %p", self, _ctx); self = [super init]; controllerCount++; useAutoCall = YES; callSelectorsMissingTrailingSemicolon = YES; canSetOnBoxedObjects= NO; logAllExceptions = NO; boxedObjects = [NSMutableDictionary new]; @synchronized(self) { if (!sharedInstanceStats) { sharedInstanceStats = [NSMutableDictionary new]; closureHash = [NSMutableDictionary new]; jsFunctionSelectors = [NSMutableDictionary new]; jsFunctionClasses = [NSMutableDictionary new]; jsFunctionHash = [NSMutableDictionary new]; splitCallCache = [NSMutableDictionary new]; jsClassParents = [NSMutableDictionary new]; // boxedObjects = [NSMutableDictionary new]; jsClasses = [NSMutableArray new]; customCallPathsCacheIsClean = NO; customCallPaths = nil; } } // // Javascript classes with our callbacks // if (!OSXObjectClass) { // // OSX object javascript definition // JSClassDefinition OSXObjectDefinition = kJSClassDefinitionEmpty; OSXObjectDefinition.className = "OSX"; OSXObjectDefinition.getProperty = OSXObject_getProperty; OSXObjectDefinition.getPropertyNames = OSXObject_getPropertyNames; OSXObjectClass = JSClassCreate(&OSXObjectDefinition); // // Private object, used for holding references to objects, classes, structs // JSClassDefinition jsCocoaObjectDefinition = kJSClassDefinitionEmpty; jsCocoaObjectDefinition.className = "JSCocoa box"; jsCocoaObjectDefinition.initialize = jsCocoaObject_initialize; jsCocoaObjectDefinition.finalize = jsCocoaObject_finalize; // jsCocoaObjectDefinition.hasProperty = jsCocoaObject_hasProperty; jsCocoaObjectDefinition.getProperty = jsCocoaObject_getProperty; jsCocoaObjectDefinition.setProperty = jsCocoaObject_setProperty; jsCocoaObjectDefinition.deleteProperty = jsCocoaObject_deleteProperty; jsCocoaObjectDefinition.getPropertyNames = jsCocoaObject_getPropertyNames; // jsCocoaObjectDefinition.callAsFunction = jsCocoaObject_callAsFunction; jsCocoaObjectDefinition.callAsConstructor = jsCocoaObject_callAsConstructor; // jsCocoaObjectDefinition.hasInstance = jsCocoaObject_hasInstance; jsCocoaObjectDefinition.convertToType = jsCocoaObject_convertToType; jsCocoaObjectClass = JSClassCreate(&jsCocoaObjectDefinition); // // Second kind of private object, used to hold method and function names // Separated from the object because "typeof NSDate.date" gave "function" instead of object, preventing enumeration in WebKit inspector // JSClassDefinition jsCocoaFunctionDefinition = kJSClassDefinitionEmpty; jsCocoaFunctionDefinition.className = "JSCocoa box"; jsCocoaFunctionDefinition.parentClass = jsCocoaObjectClass; jsCocoaFunctionDefinition.callAsFunction = jsCocoaObject_callAsFunction; jsCocoaFunctionClass = JSClassCreate(&jsCocoaFunctionDefinition); // // Holds __info in objects // JSClassDefinition jsCocoaInfoDefinition = kJSClassDefinitionEmpty; jsCocoaInfoDefinition.className = "Runtime info"; jsCocoaInfoDefinition.getProperty = jsCocoaInfo_getProperty; jsCocoaInfoDefinition.getPropertyNames = jsCocoaInfo_getPropertyNames; jsCocoaInfoClass = JSClassCreate(&jsCocoaInfoDefinition); // // Private Hash of derived classes, storing js values // JSClassDefinition jsCocoaHashObjectDefinition = kJSClassDefinitionEmpty; hashObjectClass = JSClassCreate(&jsCocoaHashObjectDefinition); } // // Start context // // Starting from our own context if (!_ctx) { ctx = JSGlobalContextCreate(OSXObjectClass); } // Starting from an existing context else { ctx = _ctx; //JSGlobalContextRetain(ctx); JSObjectRef o = JSObjectMake(ctx, OSXObjectClass, NULL); // Set a global var named 'OSX' which will fulfill the usual role of JSCocoa's global object JSStringRef jsName = JSStringCreateWithUTF8CString("OSX"); JSObjectSetProperty(ctx, JSContextGetGlobalObject(ctx), jsName, o, kJSPropertyAttributeDontDelete, NULL); JSStringRelease(jsName); [self accomodateWebKitInspector]; } #if !TARGET_IPHONE_SIMULATOR && !TARGET_OS_IPHONE [self loadFrameworkWithName:@"AppKit"]; [self loadFrameworkWithName:@"CoreFoundation"]; [self loadFrameworkWithName:@"Foundation"]; [self loadFrameworkWithName:@"CoreGraphics" inPath:@"/System/Library/Frameworks/ApplicationServices.framework/Frameworks"]; #endif #if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE [BurksPool setJSFunctionHash:jsFunctionHash]; #endif // Create a reference to ourselves, and make it read only, don't enum, don't delete [self setObjectNoRetain:self withName:@"__jsc__" attributes:kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete]; // Load class kit if (!_ctx) { useJSLint = NO; id lintPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"jslint-jscocoa" ofType:@"js"]; if ([[NSFileManager defaultManager] fileExistsAtPath:lintPath]) { BOOL b = [self evalJSFile:lintPath]; if (!b) NSLog(@"[JSCocoa initWithGlobalContext:] JSLint not loaded"); } id classKitPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"class" ofType:@"js"]; if ([[NSFileManager defaultManager] fileExistsAtPath:classKitPath]) [self evalJSFile:classKitPath]; } // Objects can use their own dealloc, normally used up by JSCocoa // JSCocoa registers 'safeDealloc' in place of 'dealloc' and calls it in the next run loop cycle. // (Dealloc might be called by JS GC, and running JS fails at this time) // useSafeDealloc will be turned to NO upon JSCocoaController dealloc useSafeDealloc = YES; // Yep ! useJSLint = YES; // ObjJ syntax renders split call moot useSplitCall = NO; ownsContext = NO; [JSCocoa updateCustomCallPaths]; return self; } - (id)init { id o = [self initWithGlobalContext:nil]; ownsContext = YES; return o; } // // Dealloc // - (void)cleanUp { // NSLog(@"JSCocoa : %p dying (ownsContext=%d)", self, ownsContext); [self setUseSafeDealloc:NO]; // Cleanup if we created the JavascriptCore context. // If not, let user do it. In a WebView, this method will be called during JS GC, // and trying to execute more JS code will fail. // User must clean up manually by calling unlinkAllReferences then destroying the webView if (ownsContext) { [self unlinkAllReferences]; JSGarbageCollect(ctx); [self setObjectNoRetain:self withName:@"__jsc__" attributes:kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete]; } controllerCount--; if (controllerCount == 0) { if (OSXObjectClass) { JSClassRelease(OSXObjectClass); JSClassRelease(jsCocoaObjectClass); JSClassRelease(jsCocoaFunctionClass); JSClassRelease(jsCocoaInfoClass); JSClassRelease(hashObjectClass); OSXObjectClass = nil; jsCocoaObjectClass = nil; jsCocoaFunctionClass = nil; jsCocoaInfoClass = nil; hashObjectClass = nil; } // We need to nil these all out, since they are static // and if we make another JSCocoaController after this- they will // still be around and that's kinda bad (like crashing bad). [sharedInstanceStats release]; sharedInstanceStats = nil; [closureHash release]; closureHash = nil; [jsFunctionSelectors release]; jsFunctionSelectors = nil; [jsFunctionClasses release]; jsFunctionClasses = nil; [jsFunctionHash release]; jsFunctionHash = nil; [splitCallCache release]; splitCallCache = nil; [jsClassParents release]; jsClassParents = nil; // [boxedObjects release]; // boxedObjects = nil; [customCallPaths release]; customCallPaths = nil; // Remove classes : go backwards to remove child classes first for (id class in [jsClasses reverseObjectEnumerator]) objc_disposeClassPair([class pointerValue]); [jsClasses release]; jsClasses = nil; } [self removeObjectWithName:@"__jsc__"]; if (ownsContext) JSGlobalContextRelease(ctx); [boxedObjects release]; } - (void)dealloc { [self cleanUp]; [super dealloc]; } - (void)finalize { [self cleanUp]; [super finalize]; } // // Shared instance // static id JSCocoaSingleton = NULL; + (id)sharedController { @synchronized(self) { if (!JSCocoaSingleton) { // 1. alloc // 2. store pointer // 3. call init // // Why ? if init is calling sharedController, the pointer won't have been set and it will call itself over and over again. // JSCocoaSingleton = [self alloc]; // NSLog(@"JSCocoa : allocating shared instance %p", JSCocoaSingleton); [JSCocoaSingleton init]; } } return JSCocoaSingleton; } + (BOOL)hasSharedController { return !!JSCocoaSingleton; } // Retrieves the __jsc__ variable from a context and unbox it + (id)controllerFromContext:(JSContextRef)ctx { JSStringRef jsName = JSStringCreateWithUTF8CString("__jsc__"); JSValueRef jsValue = JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), jsName, NULL); JSStringRelease(jsName); id jsc = nil; [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&jsc inContext:ctx]; // Commented as it falsely reports failure when controller is cleaning up while being deallocated // if (!jsc) NSLog(@"controllerFromContext couldn't find found the JSCocoaController in ctx %p", ctx); return jsc; } // Report if we're running a nightly JavascriptCore, with GC + (void)hazardReport { Dl_info info; // Get info about a JavascriptCore symbol dladdr(dlsym(RTLD_DEFAULT, "JSClassCreate"), &info); BOOL runningFromSystemLibrary = [[NSString stringWithUTF8String:info.dli_fname] hasPrefix:@"/System"]; if (!runningFromSystemLibrary) NSLog(@"***Running a nightly JavascriptCore***"); #if !TARGET_OS_IPHONE if ([NSGarbageCollector defaultCollector]) NSLog(@"***Running with ObjC Garbage Collection***"); #endif } // Report what we're running on + (NSString*)runningArchitecture { #if defined(__ppc__) return @"PPC"; // Unsupported //#elif defined(__ppc64__) // return @"PPC64"; #elif defined(__i386__) return @"i386"; #elif defined(__x86_64__) return @"x86_64"; #elif TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR return @"iPhone"; #elif TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR return @"iPhone Simulator"; #else return @"unknown architecture"; #endif } // Replace the toString function with our own - (void)accomodateWebKitInspector { // The inspector uses Object's toString to extract the class name and print it, // we replace that class name with valueOf when called for JSCocoa boxes char* script = "\ var _old_toString = Object.prototype.toString \n\ Object.prototype.toString = function () \n\ { \n\ var str = _old_toString.call(this) \n\ if (!str.match(/JSCocoa/)) \n\ return str \n\ return '[Object ' + (this.valueOf()) + ']' \n\ } \n\ "; JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)[NSString stringWithUTF8String:script]); JSEvaluateScript(ctx, scriptJS, NULL, NULL, 1, NULL); JSStringRelease(scriptJS); } #pragma mark Script evaluation // // Quick eval of strings and functions returning ObjC objects // - (id)eval:(NSString*)script { return [self toObject:[self evalJSString:script]]; } - (id)callFunction:(NSString*)name { return [self toObject:[self callJSFunctionNamed:name withArgumentsArray:nil]]; } - (id)callFunction:(NSString*)name withArguments:(NSArray*)arguments { return [self toObject:[self callJSFunctionNamed:name withArgumentsArray:arguments]]; } - (BOOL)hasFunction:(NSString*)name { return [self hasJSFunctionNamed:name]; } - (BOOL)isSyntaxValid:(NSString*)script { return [self isSyntaxValid:script error:nil]; } // // Eval of strings, functions, files, returning JavascriptCore objects // #pragma mark Script evaluation returning JavascriptCore objects // // Evaluate a file // - (BOOL)evalJSFile:(NSString*)path toJSValueRef:(JSValueRef*)returnValue { NSError* error; id script = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; // Skip .DS_Store and directories if (script == nil) return NSLog(@"evalJSFile could not open %@ (%@) — Check file encoding (should be UTF8) and file build phase (should be in \"Copy Bundle Resources\")", path, error), NO; // // Delegate canLoadJSFile // if (_delegate && [_delegate respondsToSelector:@selector(JSCocoa:canLoadJSFile:)] && ![_delegate JSCocoa:self canLoadJSFile:path]) return NO; // Expand macros script = [self expandJSMacros:script path:path]; if (!script) { NSLog(@"evalJSFile:toJSValueRef: expandJSMacros returned null on %@", path); return NO; } // // Delegate canEvaluateScript, willEvaluateScript // if (_delegate) { if ([_delegate respondsToSelector:@selector(JSCocoa:canEvaluateScript:)] && ![_delegate JSCocoa:self canEvaluateScript:script]) return NO; if ([_delegate respondsToSelector:@selector(JSCocoa:willEvaluateScript:)]) script = [_delegate JSCocoa:self willEvaluateScript:script]; } if (!customCallPathsCacheIsClean) [JSCocoa updateCustomCallPaths]; // Convert script and script URL to js strings // JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); // Using CreateWithUTF8 yields wrong results on PPC JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)script); JSStringRef scriptPath = JSStringCreateWithUTF8CString([path UTF8String]); // Eval ! JSValueRef exception = NULL; JSValueRef result = JSEvaluateScript(ctx, scriptJS, NULL, scriptPath, 1, &exception); if (returnValue) *returnValue = result; // Release JSStringRelease(scriptPath); JSStringRelease(scriptJS); if (exception) { // NSLog(@"JSException - %@", [self formatJSException:exception]); [self callDelegateForException:exception]; return NO; } return YES; } // // Evaluate a file, without caring about return result // - (BOOL)evalJSFile:(NSString*)path { return [self evalJSFile:path toJSValueRef:nil]; } // // Evaluate a string // - (JSValueRef)evalJSString:(NSString*)script withScriptPath:(NSString*)path { if (!script) return NULL; // Expand macros id expandedScript = [self expandJSMacros:script path:path]; if (expandedScript) script = expandedScript; // // Delegate canEvaluateScript, willEvaluateScript // if (_delegate) { if ([_delegate respondsToSelector:@selector(JSCocoa:canEvaluateScript:)] && ![_delegate JSCocoa:self canEvaluateScript:script]) return NULL; if ([_delegate respondsToSelector:@selector(JSCocoa:willEvaluateScript:)]) script = [_delegate JSCocoa:self willEvaluateScript:script]; } if (!script) return NSLog(@"evalJSString has nothing to eval"), NULL; if (!customCallPathsCacheIsClean) [JSCocoa updateCustomCallPaths]; JSStringRef scriptJS = JSStringCreateWithCFString((CFStringRef)script); JSValueRef exception = NULL; JSStringRef scriptPath = path ? JSStringCreateWithUTF8CString([path UTF8String]) : NULL; JSValueRef result = JSEvaluateScript(ctx, scriptJS, NULL, scriptPath, 1, &exception); JSStringRelease(scriptJS); if (path) JSStringRelease(scriptPath); if (exception) { [self callDelegateForException:exception]; return NULL; } return result; } // Evaluate a string, no script path - (JSValueRef)evalJSString:(NSString*)script { return [self evalJSString:script withScriptPath:nil]; } // // Call a Javascript function by function reference (JSValueRef) // - (JSValueRef)callJSFunction:(JSValueRef)function withArguments:(NSArray*)arguments { JSObjectRef jsFunction = JSValueToObject(ctx, function, NULL); // Return if function is not of function type if (!jsFunction) return NSLog(@"callJSFunction : value is not a function"), NULL; // Convert arguments JSValueRef* jsArguments = NULL; NSUInteger argumentCount = [arguments count]; if (argumentCount) { jsArguments = malloc(sizeof(JSValueRef)*argumentCount); for (int i=0; i= '0' && *argsParser <= '9') continue; else // Skip ObjC 'const', 'oneway' markers if (*argsParser == 'r' || *argsParser == 'V') continue; else if (*argsParser == '{') { // Parse structure encoding NSInteger count = 0; [JSCocoaFFIArgument typeEncodingsFromStructureTypeEncoding:[NSString stringWithUTF8String:argsParser] parsedCount:&count]; id encoding = [[NSString alloc] initWithBytes:argsParser length:count encoding:NSUTF8StringEncoding]; id argumentEncoding = [[JSCocoaFFIArgument alloc] init]; // Set return value if ([argumentEncodings count] == 0) [argumentEncoding setIsReturnValue:YES]; [argumentEncoding setStructureTypeEncoding:encoding]; [argumentEncodings addObject:argumentEncoding]; [argumentEncoding release]; [encoding release]; argsParser += count-1; } else { // Custom handling for pointers as they're not one char long. // char type = *argsParser; char* typeStart = argsParser; if (*argsParser == '^') while (*argsParser && !(*argsParser >= '0' && *argsParser <= '9')) argsParser++; id argumentEncoding = [[JSCocoaFFIArgument alloc] init]; // Set return value if ([argumentEncodings count] == 0) [argumentEncoding setIsReturnValue:YES]; // If pointer, copy pointer type (^i, ^{NSRect}) to the argumentEncoding if (*typeStart == '^') { id encoding = [[NSString alloc] initWithBytes:typeStart length:argsParser-typeStart encoding:NSUTF8StringEncoding]; [argumentEncoding setPointerTypeEncoding:encoding]; [encoding release]; } else { BOOL didSet = [argumentEncoding setTypeEncoding:*typeStart]; if (!didSet) { [argumentEncoding release]; return nil; } // Blocks are '@?', skip '?' if (typeStart[0] == _C_ID && typeStart[1] == _C_UNDEF) argsParser++; } [argumentEncodings addObject:argumentEncoding]; [argumentEncoding release]; } if (!*argsParser) break; } return argumentEncodings; } // // This is parsed from BridgeSupport's xml // + (NSMutableArray*)parseCFunctionEncoding:(NSString*)xml functionName:(NSString**)functionNamePlaceHolder { id argumentEncodings = [NSMutableArray array]; id xmlDocument = [[NSXMLDocument alloc] initWithXMLString:xml options:0 error:nil]; [xmlDocument autorelease]; id rootElement = [xmlDocument rootElement]; *functionNamePlaceHolder = [[rootElement attributeForName:@"name"] stringValue]; // Parse children and return value NSUInteger i, numChildren = [rootElement childCount]; id returnValue = NULL; for (i=0; i introduced because under GC, NSData gets collected early. */ + (BOOL)trySplitCall:(id*)_methodName class:(Class)class argumentCount:(size_t*)_argumentCount arguments:(JSValueRef**)_arguments ctx:(JSContextRef)c { id methodName = *_methodName; size_t argumentCount = *_argumentCount; JSValueRef* arguments = *_arguments; if (argumentCount != 1) return NO; // Get property array JSObjectRef o = JSValueToObject(c, arguments[0], NULL); if (!o) return NO; JSPropertyNameArrayRef jsNames = JSObjectCopyPropertyNames(c, o); // Convert js names to NSString names : { jsName1 : value1, jsName2 : value 2 } -> NSArray[name1, name2] id names = [NSMutableArray array]; size_t i, nameCount = JSPropertyNameArrayGetCount(jsNames); // Length of target selector = length of method + length of each (argument + ':') NSUInteger targetSelectorLength = [methodName length]; // Actual arguments JSValueRef* actualArguments = malloc(sizeof(JSValueRef)*nameCount); for (i=0; i (JS)JSObjectRef(o) --> (ObjC)BoxedJSObject(JSObjectRef(o)), // ^stored in the boxedObjects hash to always return the same box for the same object // // Create a new ObjC box around the JSValueRef boxing the JSObject // // We are returning an ObjC object to Javascript. // That ObjC object is boxed in a Javascript object. // For all boxing requests of the same ObjC object, that Javascript object needs to be unique for object comparisons to work : // NSApplication.sharedApplication == NSApplication.sharedApplication // (JavascriptCore has no hook for object to object comparison, that's why objects need to be unique) // To guarantee unicity, we keep a cache of boxed objects. // As boxed objects are JSObjectRef not derived from NSObject, we box them in an ObjC object. // // Box the ObjC object in a JSObjectRef // JSObjectRef jsObject = [JSCocoa jsCocoaPrivateObjectInContext:ctx]; JSObjectRef jsObject = [self newPrivateObject]; JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); private.type = @"@"; [private setObject:o]; // Box the JSObjectRef in our ObjC object value = [[BoxedJSObject alloc] init]; [value setJSObject:jsObject]; // Add to dictionary and make it sole owner [boxedObjects setValue:value forKey:key]; [value release]; return jsObject; } - (BOOL)isObjectBoxed:(id)o { id key = [NSString stringWithFormat:@"%p", o]; return !![boxedObjects valueForKey:key]; } - (void)deleteBoxOfObject:(id)o { id key = [NSString stringWithFormat:@"%p", o]; id value= [boxedObjects valueForKey:key]; if (!value) return; [boxedObjects removeObjectForKey:key]; } /* + (void)downBoxedJSObjectCount:(id)o { id key = [NSString stringWithFormat:@"%p", o]; id value = [boxedObjects valueForKey:key]; if (!value) return; [boxedObjects removeObjectForKey:key]; } + (id)boxedObjects { return boxedObjects; } */ #pragma mark Helpers - (id)selectorForJSFunction:(JSObjectRef)function { return [jsFunctionSelectors valueForKey:[NSString stringWithFormat:@"%p", function]]; } - (id)classForJSFunction:(JSObjectRef)function { return [jsFunctionClasses valueForKey:[NSString stringWithFormat:@"%p", function]]; } // // Given an exception, get its line number, source URL, error message and return them in a NSString // When throwing an exception from Javascript, throw an object instead of a string. // This way, JavascriptCore will add line and sourceURL. // (throw new String('error') instead of throw 'error') // + (NSString*)formatJSException:(JSValueRef)exception inContext:(JSContextRef)context { if (!exception) return @"formatJSException:(null)"; // Convert exception to string JSStringRef resultStringJS = JSValueToStringCopy(context, exception, NULL); NSString* b = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, resultStringJS); JSStringRelease(resultStringJS); [NSMakeCollectable(b) autorelease]; // Only objects contain line and source URL if (JSValueGetType(context, exception) != kJSTypeObject) return b; // Iterate over all properties of the exception JSObjectRef jsObject = JSValueToObject(context, exception, NULL); JSPropertyNameArrayRef jsNames = JSObjectCopyPropertyNames(context, jsObject); size_t i, nameCount = JSPropertyNameArrayGetCount(jsNames); id line = nil, sourceURL = nil; for (i=0; i>>evaling %@", filePath); id evaled = nil; @try { evaled = [self performSelector:sel withObject:filePath]; // NSLog(@">>>EVALED %d, %@", evaled, filePath); } @catch (id e) { NSLog(@"(Test exception from %@) %@", file, e); evaled = nil; } if (!evaled) { id error = [NSString stringWithFormat:@"test %@ failed (Ran %d out of %d tests)", file, count+1, [files count]]; [JSCocoaController log:error]; return NO; } count ++; [self garbageCollect]; } #endif return count; } - (int)runTests:(NSString*)path { return [self runTests:path withSelector:@selector(evalJSFile:)]; } #pragma mark Autorelease pool static id autoreleasePool; + (void)allocAutoreleasePool { autoreleasePool = [[NSAutoreleasePool alloc] init]; } + (void)deallocAutoreleasePool { [autoreleasePool release]; } #pragma mark Garbage Collection // // Collect on top of the run loop, not in some JS function // + (void)garbageCollect { NSLog(@"*** Deprecated — call garbageCollect on an instance ***"); /*JSGarbageCollect(NULL);*/ } - (void)garbageCollect { JSGarbageCollect(ctx); } // // Make all root Javascript variables point to null // - (void)unlinkAllReferences { // Null and delete every reference to every live object // [self evalJSString:@"for (var i in this) { log('DELETE ' + i); this[i] = null; delete this[i]; }"]; // [self evalJSString:@"for (var i in this) { this[i] = null; delete this[i]; }"]; // id del = @"var keys = Object.keys(this); var c = keys.length; for (var i=0; i", [self class], self, ((id)self == (id)[self class]) ? @"Class" : @"", [boxedObject class], boxedObject, retainCount]; */ } // NSLog(@"%@", boxedObjects); } #pragma mark Class inspection + (id)rootclasses { return [JSCocoaLib rootclasses]; } + (id)classes { return [JSCocoaLib classes]; } + (id)protocols { return [JSCocoaLib protocols]; } + (id)imageNames { return [JSCocoaLib imageNames]; } + (id)methods { return [JSCocoaLib methods]; } + (id)runtimeReport { return [JSCocoaLib runtimeReport]; } + (id)explainMethodEncoding:(id)encoding { id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:[encoding UTF8String]]; id explication = [NSMutableArray array]; for (id arg in argumentEncodings) [explication addObject:[arg typeDescription] ]; return explication; } // JSCocoa : handle setting with callMethod // object.width = 100 // -> // [object setWidth:100] // - (BOOL)JSCocoa:(JSCocoaController*)controller setProperty:(NSString*)propertyName ofObject:(id)object toValue:(JSValueRef)value inContext:(JSContextRef)localCtx exception:(JSValueRef*)exception { // FIXME: this doesn't actually work with objc properties, and we can't always rely that this method will exist either... // it should probably be moved up into the JSCocoa layer. NSString* setterName = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringWithRange:NSMakeRange(0,1)] capitalizedString], [propertyName substringWithRange:NSMakeRange(1, [propertyName length]-1)]]; if ([self JSCocoa:controller callMethod:setterName ofObject:object privateObject:nil argumentCount:1 arguments:&value inContext:localCtx exception:exception]) { return YES; } return NO; } #pragma mark Distant Object Handling (DO) // // NSDistantObject call using NSInvocation // - (JSValueRef)JSCocoa:(JSCocoaController*)controller callMethod:(NSString*)methodName ofObject:(id)callee privateObject:(JSCocoaPrivateObject*)thisPrivateObject argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments inContext:(JSContextRef)localCtx exception:(JSValueRef*)exception { SEL selector = NSSelectorFromString(methodName); if (class_getInstanceMethod([callee class], selector) || class_getClassMethod([callee class], selector)) { return nil; } NSMethodSignature *signature = [callee methodSignatureForSelector:selector]; if (!signature) { return nil; } // we need to do all this for NSDistantObject , since JSCocoa doesn't handle it natively. NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:selector]; NSUInteger argIndex = 0; while (argIndex < argumentCount) { id arg = 0x00; [JSCocoaFFIArgument unboxJSValueRef:arguments[argIndex] toObject:&arg inContext:localCtx]; const char *type = [signature getArgumentTypeAtIndex:argIndex + 2]; // Structure argument if (type && type[0] == '{') { id structureType = [NSString stringWithUTF8String:type]; id fullStructureType = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:structureType]; int size = [JSCocoaFFIArgument sizeOfStructure:structureType]; JSObjectRef jsObject = JSValueToObject(ctx, arguments[argIndex], NULL); if (size && fullStructureType && jsObject) { // Alloc structure size and let NSData deallocate it void* source = malloc(size); memset(source, 0, size); [NSData dataWithBytesNoCopy:source length:size freeWhenDone:YES]; void* p = source; NSInteger numParsed = [JSCocoaFFIArgument structureFromJSObjectRef:jsObject inContext:ctx inParentJSValueRef:NULL fromCString:(char*)[fullStructureType UTF8String] fromStorage:&p]; if (numParsed) [invocation setArgument:source atIndex:argIndex+2]; } } else if ([arg isKindOfClass:[NSNumber class]]) { // const char *type = [signature getArgumentTypeAtIndex:argIndex + 2]; if (strcmp(type, @encode(BOOL)) == 0) { BOOL b = [arg boolValue]; [invocation setArgument:&b atIndex:argIndex + 2]; } else if (strcmp(type, @encode(unsigned int)) == 0) { unsigned int i = [arg unsignedIntValue]; [invocation setArgument:&i atIndex:argIndex + 2]; } else if (strcmp(type, @encode(int)) == 0) { int i = [arg intValue]; [invocation setArgument:&i atIndex:argIndex + 2]; } else if (strcmp(type, @encode(unsigned long)) == 0) { unsigned long l = [arg unsignedLongValue]; [invocation setArgument:&l atIndex:argIndex + 2]; } else if (strcmp(type, @encode(long)) == 0) { long l = [arg longValue]; [invocation setArgument:&l atIndex:argIndex + 2]; } else if (strcmp(type, @encode(float)) == 0) { float f = [arg floatValue]; [invocation setArgument:&f atIndex:argIndex + 2]; } else if (strcmp(type, @encode(double)) == 0) { double d = [arg doubleValue]; [invocation setArgument:&d atIndex:argIndex + 2]; } else { // just do int for all else. int i = [arg intValue]; [invocation setArgument:&i atIndex:argIndex + 2]; } } else { [invocation setArgument:&arg atIndex:argIndex + 2]; } argIndex++; } @try { [invocation invokeWithTarget:callee]; } @catch (NSException * e) { NSLog(@"Exception while calling %@. %@", methodName, [e reason]); if ([[e reason] isEqualToString:@"connection went invalid while waiting for a reply"]) { // whoops? // also, how do we not look for some funky localized string here? // also also, can we now make whatever is pointing to this value, nil? if (thisPrivateObject) { NSLog(@"Connection terminated, removing reference to object"); thisPrivateObject.object = [NSNull null]; [thisPrivateObject setJSValueRef:JSValueMakeNull(localCtx) ctx:localCtx]; } } } JSValueRef jsReturnValue = NULL; const char *type = [signature methodReturnType]; if (strcmp(type, @encode(id)) == 0 || strcmp(type, @encode(Class)) == 0) { id result = 0x00; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument boxObject:result toJSValueRef:&jsReturnValue inContext:localCtx]; } /* case _C_CHR: case _C_UCHR: case _C_SHT: case _C_USHT: case _C_INT: case _C_UINT: case _C_LNG: case _C_ULNG: case _C_LNG_LNG: case _C_ULNG_LNG: case _C_FLT: case _C_DBL: */ else if (strcmp(type, @encode(char)) == 0) { char result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(char)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(unsigned char)) == 0) { unsigned char result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned char)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(short)) == 0) { short result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(short)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(unsigned short)) == 0) { unsigned short result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned short)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(int)) == 0) { int result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(int)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(unsigned int)) == 0) { unsigned int result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned int)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(long)) == 0) { long result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(long)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(unsigned long)) == 0) { unsigned long result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(unsigned long)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(float)) == 0) { float result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(float)[0] fullTypeEncoding:NULL fromStorage:&result]; } else if (strcmp(type, @encode(double)) == 0) { double result; [invocation getReturnValue:&result]; if (!result) return JSValueMakeNull(localCtx); [JSCocoaFFIArgument toJSValueRef:&jsReturnValue inContext:localCtx typeEncoding:@encode(double)[0] fullTypeEncoding:NULL fromStorage:&result]; } // Structure return else if (type && type[0] == '{') { id structureType = [NSString stringWithUTF8String:type]; id fullStructureType = [JSCocoaFFIArgument structureFullTypeEncodingFromStructureTypeEncoding:structureType]; int size = [JSCocoaFFIArgument sizeOfStructure:structureType]; if (size) { void* result = malloc(size); [invocation getReturnValue:result]; // structureToJSValueRef will advance the pointer in place, overwriting its original value void* ptr = result; NSInteger numParsed = [JSCocoaFFIArgument structureToJSValueRef:&jsReturnValue inContext:localCtx fromCString:(char*)[fullStructureType UTF8String] fromStorage:&ptr]; if (!numParsed) jsReturnValue = NULL; free(result); } } if (!jsReturnValue) return JSValueMakeNull(localCtx); return jsReturnValue; } @end #pragma mark Javascript setter functions // Give ObjC classes written in Javascript extra abilities like storing extra javascript variables in an internal __jsHash. // The following methods handle that. JSCocoaMethodHolder is a dummy class to hold them. @interface JSCocoaMethodHolder : NSObject @end @implementation JSCocoaMethodHolder - (BOOL)setJSValue:(JSValueRefAndContextRef)valueAndContext forJSName:(JSValueRefAndContextRef)nameAndContext { if (class_getInstanceVariable([self class], "__jsHash")) { JSContextRef c = valueAndContext.ctx; JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); JSObjectRef hash = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash); if (!hash) { // Retrieve controller id jsc = [JSCocoaController controllerFromContext:c]; c = [jsc ctx]; hash = JSObjectMake(c, hashObjectClass, NULL); // Same as copyWithZone: object_setInstanceVariable(self, "__jsHash", (void*)hash); object_setInstanceVariable(self, "__jsCocoaController", (void*)jsc); JSValueProtect(c, hash); [JSCocoaController upJSValueProtectCount]; [JSCocoaController upJSCocoaHashCount]; } JSObjectSetProperty(c, hash, name, valueAndContext.value, kJSPropertyAttributeNone, NULL); JSStringRelease(name); return YES; } return NO; } - (JSValueRefAndContextRef)JSValueForJSName:(JSValueRefAndContextRef)nameAndContext { JSValueRefAndContextRef valueAndContext = { JSValueMakeNull(nameAndContext.ctx), NULL }; if (class_getInstanceVariable([self class], "__jsHash")) { JSContextRef c = nameAndContext.ctx; JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); JSObjectRef hash = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash); if (!hash || !JSObjectHasProperty(c, hash, name)) { JSStringRelease(name); return valueAndContext; } valueAndContext.ctx = c; valueAndContext.value = JSObjectGetProperty(c, hash, name, NULL); JSStringRelease(name); return valueAndContext; } return valueAndContext; } - (BOOL)deleteJSValueForJSName:(JSValueRefAndContextRef)nameAndContext { if (class_getInstanceVariable([self class], "__jsHash")) { JSContextRef c = nameAndContext.ctx; JSStringRef name = JSValueToStringCopy(c, nameAndContext.value, NULL); JSObjectRef hash = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash); if (!hash || !JSObjectHasProperty(c, hash, name)) { JSStringRelease(name); return NO; } bool r = JSObjectDeleteProperty(c, hash, name, NULL); JSStringRelease(name); return r; } return NO; } // Instance count debug + (id)allocWithZone:(NSZone*)zone { // Dynamic super call id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName(self)]]; id supermetaclass = objc_getMetaClass(class_getName(parentClass)); struct objc_super superData = { self, supermetaclass }; id o = objc_msgSendSuper(&superData, @selector(allocWithZone:), zone); [JSCocoaController upInstanceCount:o]; return o; } // Called by -(id)copy - (id)copyWithZone:(NSZone *)zone { // Dynamic super call id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; struct objc_super superData = { self, parentClass }; id o = objc_msgSendSuper(&superData, @selector(copyWithZone:), zone); // // Copy hash by making a new copy // // Return if var has no controller id jsc = nil; object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); if (!jsc) return o; JSContextRef ctx = [jsc ctx]; JSObjectRef hash1 = NULL; JSObjectRef hash2 = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash1); object_getInstanceVariable(o, "__jsHash", (void**)&hash2); // Return if hash does not exist if (!hash1) return o; // Copy hash JSStringRef scriptJS = JSStringCreateWithUTF8CString("var hash1 = arguments[0]; var hash2 = {}; for (var i in hash1) hash2[i] = hash1[i]; return hash2"); JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&hash1, NULL); JSStringRelease(scriptJS); // Convert hash to object JSObjectRef hashCopy = JSValueToObject(ctx, result, NULL); object_getInstanceVariable(o, "__jsHash", (void**)&hash2); // Same as setJSValue:forJSName: // Set new hash object_setInstanceVariable(o, "__jsHash", (void*)hashCopy); object_setInstanceVariable(o, "__jsCocoaController", (void*)jsc); JSValueProtect(ctx, hashCopy); [JSCocoaController upJSValueProtectCount]; [JSCocoaController upJSCocoaHashCount]; [JSCocoaController upInstanceCount:o]; return o; } // Dealloc : unprotect js hash - (void)deallocAndCleanupJS { JSObjectRef hash = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash); if (hash) { id jsc = NULL; object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); JSValueUnprotect([jsc ctx], hash); [JSCocoaController downJSCocoaHashCount]; } [JSCocoaController downInstanceCount:self]; // Dynamic super call id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; struct objc_super superData = { self, parentClass }; objc_msgSendSuper(&superData, @selector(dealloc)); } // Finalize - same as dealloc static BOOL __warningSuppressorAsFinalizeIsCalledBy_objc_msgSendSuper = NO; - (void)finalize { JSObjectRef hash = NULL; object_getInstanceVariable(self, "__jsHash", (void**)&hash); if (hash) { id jsc = NULL; object_getInstanceVariable(self, "__jsCocoaController", (void**)&jsc); JSValueUnprotect([jsc ctx], hash); [JSCocoaController downJSCocoaHashCount]; } [JSCocoaController downInstanceCount:self]; // Dynamic super call id parentClass = [JSCocoaController parentObjCClassOfClassName:[NSString stringWithUTF8String:class_getName([self class])]]; struct objc_super superData = { self, parentClass }; objc_msgSendSuper(&superData, @selector(finalize)); // Ignore warning about missing [super finalize] as the call IS made via objc_msgSendSuper if (__warningSuppressorAsFinalizeIsCalledBy_objc_msgSendSuper) [super finalize]; } @end #pragma mark Common instance method // Class.instance == class.alloc.init + release (jsObject retains object) // Class.instance( { withA : ... andB : ... } ) == class.alloc.initWithA:... andB:... + release @implementation NSObject(CommonInstance) + (JSValueRef)instanceWithContext:(JSContextRef)ctx argumentCount:(size_t)argumentCount arguments:(JSValueRef*)arguments exception:(JSValueRef*)exception { id methodName = @"init"; JSValueRef* argumentsToFree = NULL; // Recover init method if (argumentCount == 1) { id splitMethodName = @"init"; BOOL isSplitCall = [JSCocoaController trySplitCall:&splitMethodName class:self argumentCount:&argumentCount arguments:&arguments ctx:ctx]; if (isSplitCall) { methodName = splitMethodName; argumentsToFree = arguments; } else return throwException(ctx, exception, @"Instance split call did not find an init method"), NULL; } // NSLog(@"=>Called instance on %@ with init=%@", self, methodName); // Allocate new instance id newInstance = [self alloc]; // Set it as new object // JSObjectRef thisObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; id jsc = [JSCocoa controllerFromContext:ctx]; JSObjectRef thisObject = [jsc newPrivateObject]; JSCocoaPrivateObject* private = JSObjectGetPrivate(thisObject); private.type = @"@"; [private setObjectNoRetain:newInstance]; // No — will retain allocated object and trigger "did you forget to call init" warning // Object will be automatically boxed when returned to Javascript by // JSObjectRef thisObject = [JSCocoaController boxedJSObject:newInstance inContext:ctx]; // Create function object boxing our init method // JSObjectRef function = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef function = [jsc newPrivateFunction]; private = JSObjectGetPrivate(function); private.type = @"method"; private.methodName = methodName; // Call callAsFunction on our new instance with our init method JSValueRef exceptionFromInitCall = NULL; JSValueRef returnValue = jsCocoaObject_callAsFunction(ctx, function, thisObject, argumentCount, arguments, &exceptionFromInitCall); free(argumentsToFree); if (exceptionFromInitCall) return *exception = exceptionFromInitCall, NULL; // Release object JSObjectRef returnObject = JSValueToObject(ctx, returnValue, NULL); // We can get nil when initWith... fails. (eg var image = NSImage.instance({withContentsOfFile:'DOESNOTEXIST'}) // Return nil then. if (returnObject == nil) return JSValueMakeNull(ctx); private = JSObjectGetPrivate(returnObject); id boxedObject = [private object]; [boxedObject release]; // Register our context in there so that safeDealloc finds it. if ([boxedObject respondsToSelector:@selector(safeDealloc)]) { // id jsc = [JSCocoaController controllerFromContext:ctx]; // object_setInstanceVariable(boxedObject, "__jsCocoaController", (void*)jsc); } return returnValue; } @end #pragma mark - #pragma mark JavascriptCore callbacks #pragma mark - #pragma mark JavascriptCore OSX object // // // Global resolver : main class used as 'this' in Javascript's global scope. Name requests go through here. // // JSValueRef OSXObject_getProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) { NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); [NSMakeCollectable(propertyName) autorelease]; if ([propertyName isEqualToString:@"__jsc__"]) return NULL; // NSLog(@"Asking for global property %@", propertyName); JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; id delegate = jsc.delegate; // // Delegate canGetGlobalProperty, getGlobalProperty // if (delegate) { // Check if getting is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canGetGlobalProperty:inContext:exception:)]) { BOOL canGetGlobal = [delegate JSCocoa:jsc canGetGlobalProperty:propertyName inContext:ctx exception:exception]; if (!canGetGlobal) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow getting global property %@", propertyName]); return NULL; } } // Check if delegate handles getting if ([delegate respondsToSelector:@selector(JSCocoa:getGlobalProperty:inContext:exception:)]) { JSValueRef delegateGetGlobal = [delegate JSCocoa:jsc getGlobalProperty:propertyName inContext:ctx exception:exception]; if (delegateGetGlobal) return delegateGetGlobal; } } // // ObjC class // Class objCClass = NSClassFromString(propertyName); if (objCClass && ![propertyName isEqualToString:@"Object"]) { JSValueRef ret = [jsc boxObject:objCClass]; return ret; } id xml; id type = nil; // // Query BridgeSupport for property // xml = [[BridgeSupportController sharedController] queryName:propertyName]; if (xml) { id error = nil; id xmlDocument = [[NSXMLDocument alloc] initWithXMLString:xml options:0 error:&error]; if (error) return NSLog(@"(OSX_getPropertyCallback) malformed xml while getting property %@ of type %@ : %@", propertyName, type, error), NULL; [xmlDocument autorelease]; type = [[xmlDocument rootElement] name]; // // Function // if ([type isEqualToString:@"function"]) { // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"function"; private.xml = xml; return o; } // // Struct // else if ([type isEqualToString:@"struct"]) { // JSObjectRef o = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; JSObjectRef o = [jsc newPrivateObject]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"struct"; private.xml = xml; return o; } // // Constant // else if ([type isEqualToString:@"constant"]) { // ##fix : NSZeroPoint, NSZeroRect, NSZeroSize would need special (struct) + type64 handling // Check if constant's declared_type is NSString* id declared_type = [[xmlDocument rootElement] attributeForName:@"declared_type"]; if (!declared_type) declared_type = [[xmlDocument rootElement] attributeForName:@"type"]; if (!declared_type || !([[declared_type stringValue] isEqualToString:@"NSString*"] || [[declared_type stringValue] isEqualToString:@"@"] || [[declared_type stringValue] isEqualToString:@"^{__CFString=}"] )) return NSLog(@"(OSX_getPropertyCallback) %@ not a NSString* constant : %@", propertyName, xml), NULL; // Grab symbol void* symbol = dlsym(RTLD_DEFAULT, [propertyName UTF8String]); if (!symbol) return NSLog(@"(OSX_getPropertyCallback) symbol %@ not found", propertyName), NULL; // ObjC objects, like NSApp : pointer to NSApplication.sharedApplication if ([[declared_type stringValue] isEqualToString:@"@"]) { id o = *(id*)symbol; return [jsc boxObject:o]; } // Return symbol as a Javascript string NSString* str = *(NSString**)symbol; JSStringRef jsName = JSStringCreateWithUTF8CString([str UTF8String]); JSValueRef jsString = JSValueMakeString(ctx, jsName); JSStringRelease(jsName); return jsString; } // // Enum // else if ([type isEqualToString:@"enum"]) { // Check if constant's declared_type is NSString* id value = [[xmlDocument rootElement] attributeForName:@"value"]; if (!value) { value = [[xmlDocument rootElement] attributeForName:@"value64"]; if (!value) return NSLog(@"(OSX_getPropertyCallback) %@ enum has no value set", propertyName), NULL; } // Try parsing value double doubleValue = 0; value = [value stringValue]; if (![[NSScanner scannerWithString:value] scanDouble:&doubleValue]) return NSLog(@"(OSX_getPropertyCallback) scanning %@ enum failed", propertyName), NULL; return JSValueMakeNumber(ctx, doubleValue); } } // Describe ourselves if ([propertyName isEqualToString:@"toString"] || [propertyName isEqualToString:@"valueOf"]) { JSStringRef scriptJS = JSStringCreateWithUTF8CString("return '(JSCocoa global object)'"); JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSStringRelease(scriptJS); return fn; } return NULL; } static void OSXObject_getPropertyNames(JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames) { // Move to a definition object /* NSArray* keys = [[BridgeSupportController sharedController] keys]; for (id key in keys) { JSStringRef jsString = JSStringCreateWithUTF8CString([key UTF8String]); JSPropertyNameAccumulatorAddName(propertyNames, jsString); JSStringRelease(jsString); } */ } #pragma mark JavascriptCore JSCocoa object // // Below lie the Javascript callbacks for all Javascript objects created by JSCocoa, used to pass ObjC data to and fro Javascript. // // // From PyObjC : when to call objc_msgSend_stret, for structure return // Depending on structure size & architecture, structures are returned as function first argument (done transparently by ffi) or via registers // BOOL isUsingStret(id argumentEncodings) { int resultSize = 0; char returnEncoding = [[argumentEncodings objectAtIndex:0] typeEncoding]; if (returnEncoding == _C_STRUCT_B) resultSize = [JSCocoaFFIArgument sizeOfStructure:[[argumentEncodings objectAtIndex:0] structureTypeEncoding]]; if (returnEncoding == _C_STRUCT_B && //#ifdef __ppc64__ // ffi64_stret_needs_ptr(signature_to_ffi_return_type(rettype), NULL, NULL) // //#else /* !__ppc64__ */ (resultSize > SMALL_STRUCT_LIMIT #ifdef __i386__ /* darwin/x86 ABI is slightly odd ;-) */ || (resultSize != 1 && resultSize != 2 && resultSize != 4 && resultSize != 8) #endif #ifdef __x86_64__ /* darwin/x86-64 ABI is slightly odd ;-) */ || (resultSize != 1 && resultSize != 2 && resultSize != 4 && resultSize != 8 && resultSize != 16 ) #endif ) //#endif /* !__ppc64__ */ ) { // callAddress = objc_msgSend_stret; // usingStret = YES; return YES; } return NO; } // // Return the correct objc_msgSend* variety according to encodings // void* getObjCCallAddress(id argumentEncodings) { BOOL usingStret = isUsingStret(argumentEncodings); void* callAddress = objc_msgSend; if (usingStret) callAddress = objc_msgSend_stret; #if __i386__ // || TARGET_OS_IPHONE no, iPhone uses objc_msgSend char returnEncoding = [[argumentEncodings objectAtIndex:0] typeEncoding]; if (returnEncoding == 'f' || returnEncoding == 'd') { callAddress = objc_msgSend_fpret; } #endif return callAddress; } // // Convert FROM a webView context to a local context (called by valueOf(), toString()) // JSValueRef valueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx) { int type = JSValueGetType(externalCtx, value); switch (type) { case kJSTypeUndefined: { return JSValueMakeUndefined(ctx); } case kJSTypeNull: { return JSValueMakeNull(ctx); } case kJSTypeBoolean: { bool b = JSValueToBoolean(externalCtx, value); return JSValueMakeBoolean(ctx, b); } case kJSTypeNumber: { double d = JSValueToNumber(externalCtx, value, NULL); return JSValueMakeNumber(ctx, d); } // Make strings and objects show up only as strings case kJSTypeString: case kJSTypeObject: { // Add an (externalContext) suffix to distinguish boxed JSValues from a WebView JSStringRef jsString = JSValueToStringCopy(externalCtx, value, NULL); NSString* string = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, jsString); NSString* idString; // Mark only objects as (externalContext), not raw strings if (type == kJSTypeObject) idString = [NSString stringWithFormat:@"%@ (externalContext)", string]; else idString = [NSString stringWithFormat:@"%@", string]; [string release]; JSStringRelease(jsString); jsString = JSStringCreateWithUTF8CString([idString UTF8String]); JSValueRef returnValue = JSValueMakeString(ctx, jsString); JSStringRelease(jsString); return returnValue; } } return JSValueMakeNull(ctx); } // // Convert TO a webView context from a local context // JSValueRef valueToExternalContext(JSContextRef ctx, JSValueRef value, JSContextRef externalCtx) { int type = JSValueGetType(ctx, value); switch (type) { case kJSTypeUndefined: { return JSValueMakeUndefined(externalCtx); } case kJSTypeNull: { return JSValueMakeNull(externalCtx); } case kJSTypeBoolean: { bool b = JSValueToBoolean(ctx, value); return JSValueMakeBoolean(externalCtx, b); } case kJSTypeNumber: { double d = JSValueToNumber(ctx, value, NULL); return JSValueMakeNumber(externalCtx, d); } case kJSTypeString: { JSStringRef jsString = JSValueToStringCopy(ctx, value, NULL); JSValueRef returnValue = JSValueMakeString(externalCtx, jsString); JSStringRelease(jsString); return returnValue; } case kJSTypeObject: { JSObjectRef o = JSValueToObject(ctx, value, NULL); if (!o) return JSValueMakeNull(externalCtx); JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(o); if (![privateObject.type isEqualToString:@"externalJSValueRef"]) { id object = [privateObject object]; if ([object isKindOfClass:[NSString class]]) { JSStringRef jsName = JSStringCreateWithUTF8CString([object UTF8String]); JSValueRef jsString = JSValueMakeString(externalCtx, jsName); JSStringRelease(jsName); return jsString; } if ([object isKindOfClass:[NSNumber class]]) { return JSValueMakeNumber(externalCtx, [object doubleValue]); } // NSLog(@"Object (%@) converted to undefined", o ); return JSValueMakeUndefined(externalCtx); } return [privateObject jsValueRef]; } } return JSValueMakeNull(externalCtx); } JSValueRef boxedValueFromExternalContext(JSContextRef externalCtx, JSValueRef value, JSContextRef ctx) { if (JSValueGetType(externalCtx, value) < kJSTypeObject) return valueFromExternalContext(externalCtx, value, ctx); // If value is function ... JSStringRef scriptJS= JSStringCreateWithUTF8CString("return (typeof arguments[0]) == 'function' ? true : null"); JSObjectRef fn = JSObjectMakeFunction(externalCtx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef result = JSObjectCallAsFunction(externalCtx, fn, NULL, 1, (JSValueRef*)&value, NULL); JSStringRelease(scriptJS); // ... use the function boxer JSObjectRef o; if (JSValueIsBoolean(externalCtx, result)) // o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; o = [[JSCocoa controllerFromContext:ctx] newPrivateFunction]; else // o = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; o = [[JSCocoa controllerFromContext:ctx] newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"externalJSValueRef"; [private setExternalJSValueRef:value ctx:externalCtx]; return o; } // // valueOf : from a boxed ObjC object, returns a primitive javascript value (number or string) // that JavascriptCore can use in expressions (eg boxedObject + 'this', boxedObject < 4) // // The returned value is temporary and does not affect the boxed object. // JSValueRef valueOfCallback(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef *exception) { // Holding a native JS value ? Return it JSCocoaPrivateObject* thisPrivateObject = JSObjectGetPrivate(thisObject); if ([thisPrivateObject.type isEqualToString:@"jsValueRef"]) { return [thisPrivateObject jsValueRef]; } // External jsValueRef from WebView if ([thisPrivateObject.type isEqualToString:@"externalJSValueRef"]) { JSContextRef externalCtx = [thisPrivateObject ctx]; JSValueRef externalJSValueRef = [thisPrivateObject jsValueRef]; JSStringRef scriptJS= JSStringCreateWithUTF8CString("return arguments[0].valueOf()"); JSObjectRef fn = JSObjectMakeFunction(externalCtx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef result = JSObjectCallAsFunction(externalCtx, fn, NULL, 1, (JSValueRef*)&externalJSValueRef, NULL); JSStringRelease(scriptJS); return valueFromExternalContext(externalCtx, result, ctx); } // NSNumber special case if ([thisPrivateObject.object isKindOfClass:[NSNumber class]]) return JSValueMakeNumber(ctx, [thisPrivateObject.object doubleValue]); // Convert to string id toString = [thisPrivateObject description]; // Object if ([thisPrivateObject.type isEqualToString:@"@"]) { // Holding an out value ? if ([thisPrivateObject.object isKindOfClass:[JSCocoaOutArgument class]]) { JSValueRef outValue = [(JSCocoaOutArgument*)thisPrivateObject.object outJSValueRefInContext:ctx]; if (!outValue) { JSStringRef jsName = JSStringCreateWithUTF8CString("Unitialized outArgument"); JSValueRef r = JSValueMakeString(ctx, jsName); JSStringRelease(jsName); return r; } // Holding an object ? Call valueOf on it if (JSValueGetType(ctx, outValue) == kJSTypeObject) return valueOfCallback(ctx, NULL, JSValueToObject(ctx, outValue, NULL), 0, NULL, NULL); // Return raw JSValueRef return outValue; } else toString = [NSString stringWithFormat:@"%@", [[thisPrivateObject object] description]]; } // Struct if ([thisPrivateObject.type isEqualToString:@"struct"]) { id structDescription = nil; id self = [JSCocoaController controllerFromContext:ctx]; if ([self hasJSFunctionNamed:@"describeStruct"]) { JSStringRef scriptJS = JSStringCreateWithUTF8CString("return describeStruct(arguments[0])"); JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef jsValue = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&thisObject, NULL); JSStringRelease(scriptJS); [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&structDescription inContext:ctx]; } toString = [NSString stringWithFormat:@"<%@ %@>", thisPrivateObject.structureName, structDescription]; } // Return a number is the whole string (no spaces, no others chars) is a number // This emulates the javascript behaviour '4'*2 -> 8 when '4' is a string or an NSString NSScanner* scan = [NSScanner scannerWithString:toString]; [scan setCharactersToBeSkipped:nil]; double v = 0; [scan scanDouble:&v]; if ([scan isAtEnd]) return JSValueMakeNumber(ctx, v); // Convert to string and return JSStringRef jsToString = JSStringCreateWithCFString((CFStringRef)toString); JSValueRef jsValueToString = JSValueMakeString(ctx, jsToString); JSStringRelease(jsToString); return jsValueToString; } // // initialize // retain boxed object // static void jsCocoaObject_initialize(JSContextRef ctx, JSObjectRef object) { id o = JSObjectGetPrivate(object); [o retain]; } // // finalize // release boxed object // static void jsCocoaObject_finalize(JSObjectRef object) { // NSLog(@"finalizing %p", object); // If dealloc is overloaded, releasing now will trigger JS code and fail // As we're being called by GC, KJS might assert() in operationInProgress == NoOperation JSCocoaPrivateObject* private = JSObjectGetPrivate(object); // Clean up the object now as WebKit calls us twice while cleaning __jsc__ (20110730) JSObjectSetPrivate(object, NULL); id jsc = nil; JSContextRef ctx = [private ctx]; if (ctx) jsc = [JSCocoa controllerFromContext:ctx]; // We will be called during garbage collection before dealloc occurs. // The __jsc__ variable will be gone, therefore controllerFromContext will yield 0. // Not a problem since it's only used to remove the object from the boxedObjects hash, // and dealloc will occur soon after. // // If a boxed object is being destroyed, remove it from the cache // id boxedObject = [private object]; if (boxedObject) { if ([jsc isObjectBoxed:boxedObject]) { // Safe dealloc ? if ([boxedObject retainCount] == 1) { if ([boxedObject respondsToSelector:@selector(safeDealloc)]) { jsc = NULL; object_getInstanceVariable(boxedObject, "__jsCocoaController", (void**)&jsc); // Call safeDealloc if enabled (will be disabled upon last JSCocoaController release, to make sure the ) if (jsc) { if ([jsc useSafeDealloc]) [jsc performSelector:@selector(safeDeallocInstance:) withObject:boxedObject afterDelay:0]; } else NSLog(@"safeDealloc could not find the context attached to %@.%p - allocate this object with instance, or add a Javascript variable to it (obj.hello = 'world')", [boxedObject class], boxedObject); } } [jsc deleteBoxOfObject:boxedObject]; } } // Immediate release if dealloc is not overloaded [private release]; #ifdef __OBJC_GC__ // Mark internal object as collectable [[NSGarbageCollector defaultCollector] enableCollectorForPointer:private]; #endif } /* // // Not needed as getProperty can return NULL to indicate property inexistance. // // log('doesNotExist' in object) // getProperty returning undefined would mean the key is defined and has an undefined value. // getProperty therefore returns NULL and the in operator returns false. // -> hasProperty not needed. // static bool jsCocoaObject_hasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS) { NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); [NSMakeCollectable(propertyName) autorelease]; NSLog(@"hasProperty %@", propertyName); return jsCocoaObject_getProperty(ctx, object, propertyNameJS, NULL); return YES; } */ // // getProperty // Return property in object's internal Javascript hash if its contains propertyName // else ... // Get objC method matching propertyName, autocall it // else ... // method may be a split call -> return a private object // // At method start, handle special cases for arrays (integers, length) and dictionaries // static JSValueRef jsCocoaObject_getProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) { NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); [NSMakeCollectable(propertyName) autorelease]; JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); // NSLog(@"Asking for property %@ %@(%@)", propertyName, privateObject, privateObject.type); // Get delegate JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; id delegate = jsc.delegate; if ([privateObject.type isEqualToString:@"@"]) { call: // // Delegate canGetProperty, getProperty // if (delegate) { // Check if getting is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canGetProperty:ofObject:inContext:exception:)]) { BOOL canGet = [delegate JSCocoa:jsc canGetProperty:propertyName ofObject:privateObject.object inContext:ctx exception:exception]; if (!canGet) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow getting %@.%@", privateObject.object, propertyName]); return NULL; } } // Check if delegate handles getting if ([delegate respondsToSelector:@selector(JSCocoa:getProperty:ofObject:inContext:exception:)]) { JSValueRef delegateGet = [delegate JSCocoa:jsc getProperty:propertyName ofObject:privateObject.object inContext:ctx exception:exception]; if (delegateGet) return delegateGet; } } // Special case for NSMutableArray get and Javascript array methods // if ([privateObject.object isKindOfClass:[NSArray class]]) // Use respondsToSelector for custom indexed access if ([privateObject.object respondsToSelector:@selector(objectAtIndex:)]) { id array = privateObject.object; id scan = [NSScanner scannerWithString:propertyName]; NSInteger propertyIndex; // Is asked property an int ? BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); if (convertedToInt && [scan isAtEnd]) { if (propertyIndex < 0 || propertyIndex >= [array count]) return NULL; id o = [array objectAtIndex:propertyIndex]; JSValueRef value = NULL; [JSCocoaFFIArgument boxObject:o toJSValueRef:&value inContext:ctx]; return value; } // If we have 'length', switch it to 'count' if ([propertyName isEqualToString:@"length"]) propertyName = @"count"; // NSArray bridge id callee = [privateObject object]; SEL sel = NSSelectorFromString(propertyName); if ([propertyName rangeOfString:@":"].location == NSNotFound && ![callee respondsToSelector:sel] && ![propertyName isEqualToString:@"valueOf"] && ![propertyName isEqualToString:@"toString"] ) { id script = [NSString stringWithFormat:@"return Array.prototype.%@", propertyName]; JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 0, NULL, NULL); JSStringRelease(scriptJS); BOOL isJavascriptArrayMethod = result ? !JSValueIsUndefined(ctx, result) : NO; // Return the packaged Javascript function if (isJavascriptArrayMethod) { // NSLog(@"*** array method : %@", propertyName); // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"jsFunction"; [private setJSValueRef:result ctx:ctx]; return o; } } } // Special case for NSMutableDictionary get // if ([privateObject.object isKindOfClass:[NSDictionary class]]) // Use respondsToSelector for custom indexed access if ([privateObject.object respondsToSelector:@selector(objectForKey:)]) { id dictionary = privateObject.object; id o = [dictionary objectForKey:propertyName]; if (o) { JSValueRef value = NULL; [JSCocoaFFIArgument boxObject:o toJSValueRef:&value inContext:ctx]; return value; } } // Special case for JSCocoaMemoryBuffer get if ([privateObject.object isKindOfClass:[JSCocoaMemoryBuffer class]]) { id buffer = privateObject.object; id scan = [NSScanner scannerWithString:propertyName]; NSInteger propertyIndex; // Is asked property an int ? BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); if (convertedToInt && [scan isAtEnd]) { if (propertyIndex < 0 || propertyIndex >= [buffer typeCount]) return NULL; return [buffer valueAtIndex:propertyIndex inContext:ctx]; } } // Check object's internal property in its jsHash id callee = [privateObject object]; if ([callee respondsToSelector:@selector(JSValueForJSName:)]) { JSValueRefAndContextRef name = { JSValueMakeString(ctx, propertyNameJS), ctx } ; JSValueRef hashProperty = [callee JSValueForJSName:name].value; if (hashProperty && !JSValueIsNull(ctx, hashProperty)) { BOOL returnHashValue = YES; // Make sure to not return hash value if it's native code (valueOf, toString) if ([propertyName isEqualToString:@"valueOf"] || [propertyName isEqualToString:@"toString"]) { id script = [NSString stringWithFormat:@"return arguments[0].toString().indexOf('[native code]') != -1", propertyName]; JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]); JSObjectRef fn = JSObjectMakeFunction(ctx, NULL, 0, NULL, scriptJS, NULL, 1, NULL); JSValueRef result = JSObjectCallAsFunction(ctx, fn, NULL, 1, (JSValueRef*)&hashProperty, NULL); JSStringRelease(scriptJS); BOOL isNativeCode = result ? JSValueToBoolean(ctx, result) : NO; returnHashValue = !isNativeCode; // NSLog(@"isNative(%@)=%d rawJSResult=%p hashProperty=%p returnHashValue=%d", propertyName, isNativeCode, result, hashProperty, returnHashValue); } if (returnHashValue) return hashProperty; } } /* // ## Use javascript override functions, only bridge side. Discarded for now as it doesn't give a way to call the original method // ## Plus : useful ? as it can be done by setting custom js functions on the boxed objects // Check if this is a Javascript override id script = [NSString stringWithFormat:@"__globalJSFunctionRepository__.%@.%@", [callee class], propertyName]; JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); JSValueRef result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); JSStringRelease(jsScript); if (result && JSValueGetType(ctx, result) == kJSTypeObject) { NSLog(@"GOT IT %@", propertyName); JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"jsFunction"; [private setJSValueRef:result ctx:ctx]; return o; } */ // // Attempt Zero arg autocall // Object.alloc().init() -> Object.alloc.init // if ([jsc useAutoCall]) { callee = [privateObject object]; SEL sel = NSSelectorFromString(propertyName); BOOL isInstanceCall = [propertyName isEqualToString:@"instance"]; // Go for zero arg call if ([propertyName rangeOfString:@":"].location == NSNotFound && ([callee respondsToSelector:sel] || isInstanceCall)) { // // Delegate canCallMethod, callMethod // if (delegate) { // Check if calling is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) { BOOL canCall = [delegate JSCocoa:jsc canCallMethod:propertyName ofObject:callee argumentCount:0 arguments:NULL inContext:ctx exception:exception]; if (!canCall) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, propertyName]); return NULL; } } // Check if delegate handles calling if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) { JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:propertyName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; if (delegateCall) return delegateCall; } } // instance if (isInstanceCall) { // Manually call and box our object id class = [callee class]; id instance = [[class alloc] init]; JSValueRef returnValue; [JSCocoaFFIArgument boxObject:instance toJSValueRef:&returnValue inContext:ctx]; // Release it, making the javascript box the sole retainer // Nulling all references to this object will release the instance during Javascript GC JSCocoaPrivateObject* private = JSObjectGetPrivate(JSValueToObject(ctx, returnValue, NULL)); [private.object release]; return returnValue; } // Special case for alloc autocall — do not retain alloced result as it might crash (eg [[NSLocale alloc] retain] fails in ObjC) if ([propertyName isEqualToString:@"alloc"]) { id allocatedObject = [callee alloc]; // JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; JSObjectRef jsObject = [jsc newPrivateObject]; JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); private.type = @"@"; [private setObjectNoRetain:allocatedObject]; return jsObject; } // Get method pointer Method method = class_getInstanceMethod([callee class], sel); if (!method) method = class_getClassMethod([callee class], sel); // If we didn't find a method, try Distant Object if (!method) { JSValueRef res = [jsc JSCocoa:jsc callMethod:propertyName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; if (res) return res; throwException(ctx, exception, [NSString stringWithFormat:@"Could not get property[%@ %@]", callee, propertyName]); return NULL; } // Extract arguments const char* typeEncoding = method_getTypeEncoding(method); id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; // Call address void* callAddress = getObjCCallAddress(argumentEncodings); // // ffi data // ffi_cif cif; ffi_type* args[2]; void* values[2]; char* selector; selector = (char*)NSSelectorFromString(propertyName); args[0] = &ffi_type_pointer; args[1] = &ffi_type_pointer; values[0] = (void*)&callee; values[1] = (void*)&selector; // Get return value holder id returnValue = [argumentEncodings objectAtIndex:0]; // Allocate return value storage if it's a pointer if ([returnValue typeEncoding] == '^') [returnValue allocateStorage]; // Setup ffi ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 2, [returnValue ffi_type], args); // // Call ! // if (prep_status == FFI_OK) { void* storage = [returnValue storage]; if ([returnValue ffi_type] == &ffi_type_void) storage = NULL; ffi_call(&cif, callAddress, storage, values); } // Return now if our function returns void // NO - box it // if ([returnValue ffi_type] == &ffi_type_void) return NULL; // Else, convert return value JSValueRef jsReturnValue = NULL; BOOL converted = [returnValue toJSValueRef:&jsReturnValue inContext:ctx]; if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"Return value not converted in %@", propertyName]), NULL; return jsReturnValue; } } // Check if we're holding an out value if ([privateObject.object isKindOfClass:[JSCocoaOutArgument class]]) { JSValueRef outValue = [(JSCocoaOutArgument*)privateObject.object outJSValueRefInContext:ctx]; if (outValue && JSValueGetType(ctx, outValue) == kJSTypeObject) { JSObjectRef outObject = JSValueToObject(ctx, outValue, NULL); JSValueRef possibleReturnValue = JSObjectGetProperty(ctx, outObject, propertyNameJS, NULL); return possibleReturnValue; } } // Info object for instances and classes if ([propertyName isEqualToString:@RuntimeInformationPropertyName]) { JSObjectRef o = JSObjectMake(ctx, jsCocoaInfoClass, NULL); JSStringRef classNameProperty = JSStringCreateWithUTF8CString("className"); JSStringRef className = JSStringCreateWithUTF8CString([[[[privateObject object] class] description] UTF8String]); JSObjectSetProperty(ctx, o, classNameProperty, JSValueMakeString(ctx, className), kJSPropertyAttributeReadOnly|kJSPropertyAttributeDontEnum|kJSPropertyAttributeDontDelete, NULL); JSStringRelease(classNameProperty); JSStringRelease(className); return o; } // // We're asked a property name and at this point we've checked the class's jsarray, autocall. // If the property we're asked does not start a split call we'll return NULL. // // Check if the property is actually a method. // If NO, replace underscores with colons // add a ':' suffix // // If callee still fails to responds to that, check if propertyName starts a split call. // If NO, return null // id methodName = [NSMutableString stringWithString:propertyName]; // If responds to selector, OK if (![callee respondsToSelector:NSSelectorFromString(methodName)] // non ObjC methods && ![methodName isEqualToString:@"valueOf"] && ![methodName isEqualToString:@"Super"] && ![methodName isEqualToString:@"Original"] /* && ![methodName isEqualToString:@"instance"]*/) { // If setting on boxed objects is allowed, check existence of a property set on the js object - this is a reentrant call if ([jsc canSetOnBoxedObjects]) { // We need to bypass our get handler to get the js value static int canSetCheck = 0; // Return NULL so the get handler will retrieve the js property stored in the js object if (canSetCheck > 0) return NULL; canSetCheck++; // Call default handler JSValueRef jsValueSetOnBoxedObject = JSObjectGetProperty(ctx, object, propertyNameJS, nil); canSetCheck--; // If we have something other than undefined, return it if (JSValueGetType(ctx, jsValueSetOnBoxedObject) != kJSTypeUndefined) return jsValueSetOnBoxedObject; } if ([methodName rangeOfString:@"_"].location != NSNotFound) [methodName replaceOccurrencesOfString:@"_" withString:@":" options:0 range:NSMakeRange(0, [methodName length])]; if ([jsc callSelectorsMissingTrailingSemicolon] && ![methodName hasSuffix:@":"]) [methodName appendString:@":"]; if (![callee respondsToSelector:NSSelectorFromString(methodName)]) { // Instance check if ([methodName hasPrefix:@"instance"]) { id initMethodName = [NSString stringWithFormat:@"init%@", [methodName substringFromIndex:8]]; if ([callee instancesRespondToSelector:NSSelectorFromString(initMethodName)]) { // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"method"; private.methodName = methodName; return o; } } // // This may be a JS function // Class class = [callee class]; JSValueRef result = NULL; while (class) { id script = [NSString stringWithFormat:@"__globalJSFunctionRepository__.%@.%@", class, propertyName]; JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); JSStringRelease(jsScript); // Found ? Break if (result && JSValueGetType(ctx, result) == kJSTypeObject) break; // Go up parent class class = [class superclass]; } // This is a pure JS function call — box it if (result && JSValueGetType(ctx, result) == kJSTypeObject) { // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"jsFunction"; [private setJSValueRef:result ctx:ctx]; return o; } methodName = propertyName; // Get the meta class if callee is a class class = [callee class]; if (callee == class) class = objc_getMetaClass(object_getClassName(class)); // Try split start BOOL isMaybeSplit = NO; if ([jsc useSplitCall]) isMaybeSplit = [JSCocoaController isMaybeSplitCall:methodName forClass:class]; // If not split and not NSString, return (if NSString, try to convert to JS string in callAsFunction and use native JS methods) if (!isMaybeSplit && ![callee isKindOfClass:[NSString class]]) { return NULL; } } } // Get ready for method call // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"method"; private.methodName = methodName; return o; } // Struct + rawPointer valueOf if (/*[privateObject.type isEqualToString:@"struct"] &&*/ ([propertyName isEqualToString:@"valueOf"] || [propertyName isEqualToString:@"toString"])) { // JSObjectRef o = [JSCocoaController jsCocoaPrivateFunctionInContext:ctx]; JSObjectRef o = [jsc newPrivateFunction]; JSCocoaPrivateObject* private = JSObjectGetPrivate(o); private.type = @"method"; private.methodName = propertyName; return o; } // Pointer ops // * If we have an external Javascript context, query it // * Handle pointer reference / dereference with JSCocoaFFIArgument if ([privateObject.type isEqualToString:@"rawPointer"]) { BOOL responds = NO; id methodName = propertyName; responds = [privateObject respondsToSelector:NSSelectorFromString(propertyName)]; if (!responds) { methodName = [NSString stringWithFormat:@"%@:", methodName]; responds = [privateObject respondsToSelector:NSSelectorFromString(methodName)]; } if (responds) { // When calling a method with arguments, this will be used to get the instance on which to call id callee = privateObject; // Retaining the object leaks [privateObject setObjectNoRetain:privateObject]; privateObject = [[JSCocoaPrivateObject new] autorelease]; privateObject.object = callee; privateObject.type = @"@"; goto call; } } // External WebView value if ([privateObject.type isEqualToString:@"externalJSValueRef"] || [[privateObject rawPointerEncoding] isEqualToString:@"^{OpaqueJSContext=}"]) { JSValueRef externalValue = [privateObject jsValueRef]; JSContextRef externalCtx = externalValue ? [privateObject ctx] : [privateObject rawPointer]; JSObjectRef externalObject = externalValue ? JSValueToObject(externalCtx, externalValue, NULL) : JSContextGetGlobalObject(externalCtx); if (!JSObjectHasProperty(externalCtx, externalObject, propertyNameJS)) return NULL; JSValueRef r = JSObjectGetProperty(externalCtx, externalObject, propertyNameJS, exception); // If WebView had an exception, re-throw it in our context if (exception && *exception) { id s = [JSCocoaController formatJSException:*exception inContext:externalCtx]; throwException(ctx, exception, [NSString stringWithFormat:@"(WebView) %@", s]); return JSValueMakeNull(ctx); } JSValueRef r2 = boxedValueFromExternalContext(externalCtx, r, ctx); return r2; } // Structs will get here when being asked javascript attributes (eg 'x' in point.x) // NSLog(@"Asking for property %@ %@(%@)", propertyName, privateObject, privateObject.type); return NULL; } // // setProperty // call setter : propertyName -> setPropertyName // static bool jsCocoaObject_setProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef jsValue, JSValueRef* exception) { JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); [NSMakeCollectable(propertyName) autorelease]; // NSLog(@"****SET %@ in ctx %p on object %p (type=%@) method=%@", propertyName, ctx, object, privateObject.type, privateObject.methodName); // Get delegate JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; id delegate = jsc.delegate; if ([privateObject.type isEqualToString:@"@"]) { // // Delegate canSetProperty, setProperty // if (delegate) { // Check if setting is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canSetProperty:ofObject:toValue:inContext:exception:)]) { BOOL canSet = [delegate JSCocoa:jsc canSetProperty:propertyName ofObject:privateObject.object toValue:jsValue inContext:ctx exception:exception]; if (!canSet) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow setting %@.%@", privateObject.object, propertyName]); return NULL; } } // Check if delegate handles getting if ([delegate respondsToSelector:@selector(JSCocoa:setProperty:ofObject:toValue:inContext:exception:)]) { BOOL delegateSet = [delegate JSCocoa:jsc setProperty:propertyName ofObject:privateObject.object toValue:jsValue inContext:ctx exception:exception]; if (delegateSet) return true; } } // Special case for NSMutableArray set // if ([privateObject.object isKindOfClass:[NSArray class]]) if ([privateObject.object respondsToSelector:@selector(replaceObjectAtIndex:withObject:)]) { id array = privateObject.object; // if (![array respondsToSelector:@selector(replaceObjectAtIndex:withObject:)]) return throwException(ctx, exception, @"Calling set on a non mutable array"), false; id scan = [NSScanner scannerWithString:propertyName]; NSInteger propertyIndex; // Is asked property an int ? BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); if (convertedToInt && [scan isAtEnd]) { if (propertyIndex < 0 || propertyIndex >= [array count]) return false; id property = NULL; if ([JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&property inContext:ctx]) { [array replaceObjectAtIndex:propertyIndex withObject:property]; return true; } else return false; } } // Special case for NSMutableDictionary set // if ([privateObject.object isKindOfClass:[NSDictionary class]]) if ([privateObject.object respondsToSelector:@selector(setObject:forKey:)]) { id dictionary = privateObject.object; // if (![dictionary respondsToSelector:@selector(setObject:forKey:)]) return throwException(ctx, exception, @"Calling set on a non mutable dictionary"), false; id property = NULL; if ([JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&property inContext:ctx]) { [dictionary setObject:property forKey:propertyName]; return true; } else return false; } // Special case for JSCocoaMemoryBuffer get if ([privateObject.object isKindOfClass:[JSCocoaMemoryBuffer class]]) { id buffer = privateObject.object; id scan = [NSScanner scannerWithString:propertyName]; NSInteger propertyIndex; // Is asked property an int ? BOOL convertedToInt = ([scan scanInteger:&propertyIndex]); if (convertedToInt && [scan isAtEnd]) { if (propertyIndex < 0 || propertyIndex >= [buffer typeCount]) return NULL; return [buffer setValue:jsValue atIndex:propertyIndex inContext:ctx]; } } // Try shorthand overload : obc[selector] = function id callee = [privateObject object]; if ([propertyName rangeOfString:@":"].location != NSNotFound) { JSValueRefAndContextRef v = { jsValue, ctx }; [JSCocoaController overloadInstanceMethod:propertyName class:[callee class] jsFunction:v]; return true; } // Can't use capitalizedString on the whole string as it will transform // myValue // to Myvalue (thereby destroying camel letters) // we want MyValue // Capitalize only first letter NSString* setterName = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringWithRange:NSMakeRange(0,1)] capitalizedString], [propertyName substringWithRange:NSMakeRange(1, [propertyName length]-1)]]; // // Attempt Zero arg autocall for setter // Object.alloc().init() -> Object.alloc.init // SEL sel = NSSelectorFromString(setterName); if ([callee respondsToSelector:sel]) { // // Delegate canCallMethod, callMethod // if (delegate) { // Check if calling is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) { BOOL canCall = [delegate JSCocoa:jsc canCallMethod:setterName ofObject:callee argumentCount:0 arguments:NULL inContext:ctx exception:exception]; if (!canCall) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, setterName]); return NULL; } } // Check if delegate handles calling if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) { JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:setterName ofObject:callee privateObject:privateObject argumentCount:0 arguments:NULL inContext:ctx exception:exception]; if (delegateCall) return !!delegateCall; } } // Get method pointer Method method = class_getInstanceMethod([callee class], sel); if (!method) method = class_getClassMethod([callee class], sel); // If we didn't find a method, try Distant Object if (!method) { // Last chance before exception : try calling DO BOOL b = [jsc JSCocoa:jsc setProperty:propertyName ofObject:callee toValue:jsValue inContext:ctx exception:exception]; if (b) return YES; throwException(ctx, exception, [NSString stringWithFormat:@"Could not set property[%@ %@]", callee, propertyName]); return NULL; } // Extract arguments const char* typeEncoding = method_getTypeEncoding(method); id argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; if ([[argumentEncodings objectAtIndex:0] typeEncoding] != 'v') return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) %@ must return void", setterName]), false; // Call address void* callAddress = getObjCCallAddress(argumentEncodings); // // ffi data // ffi_cif cif; ffi_type* args[3]; void* values[3]; char* selector; selector = (char*)NSSelectorFromString(setterName); args[0] = &ffi_type_pointer; args[1] = &ffi_type_pointer; values[0] = (void*)&callee; values[1] = (void*)&selector; // Get arg (skip return value, instance, selector) JSCocoaFFIArgument* arg = [argumentEncodings objectAtIndex:3]; BOOL converted = [arg fromJSValueRef:jsValue inContext:ctx]; if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) Argument %c not converted", [arg typeEncoding]]), false; args[2] = [arg ffi_type]; values[2] = [arg storage]; // Setup ffi ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, 3, &ffi_type_void, args); // // Call ! // if (prep_status == FFI_OK) { ffi_call(&cif, callAddress, NULL, values); } return true; } if ([callee respondsToSelector:@selector(setJSValue:forJSName:)]) { // Set as instance variable // BOOL set = [callee setJSValue:jsValue forJSName:propertyNameJS]; JSValueRefAndContextRef value = { JSValueMakeNull(ctx), ctx }; value.value = jsValue; JSValueRefAndContextRef name = { JSValueMakeNull(ctx), ctx } ; name.value = JSValueMakeString(ctx, propertyNameJS); BOOL set = [callee setJSValue:value forJSName:name]; if (set) return true; } } // External WebView value if ([privateObject.type isEqualToString:@"externalJSValueRef"] || [[privateObject rawPointerEncoding] isEqualToString:@"^{OpaqueJSContext=}"]) { JSValueRef externalValue = [privateObject jsValueRef]; JSContextRef externalCtx = externalValue ? [privateObject ctx] : [privateObject rawPointer]; JSObjectRef externalObject = externalValue ? JSValueToObject(externalCtx, externalValue, NULL) : JSContextGetGlobalObject(externalCtx); if (!externalObject) return false; JSValueRef convertedValue = valueToExternalContext(ctx, jsValue, externalCtx); JSObjectSetProperty(externalCtx, externalObject, propertyNameJS, convertedValue, kJSPropertyAttributeNone, exception); // If WebView had an exception, re-throw it in our context if (exception && *exception) { id s = [JSCocoaController formatJSException:*exception inContext:externalCtx]; throwException(ctx, exception, [NSString stringWithFormat:@"(WebView) %@", s]); return false; } return true; } // // From here we return false to have Javascript set values on Javascript objects : valueOf, thisObject, structures // // Special case for autocall : allow current js object to receive a custom valueOf method that will handle autocall // And a thisObject property holding class for instance autocall if ([propertyName isEqualToString:@"valueOf"]) return false; // An out argument allocates pointer storage when calling stuff like gl version. // JSCocoa needs to set a custom javascript property to recognize out arguments. if ([propertyName isEqualToString:@"isOutArgument"]) return false; // Allow general setting on structs if ([privateObject.type isEqualToString:@"struct"]) return false; // Don't throw an exception if setting is allowed if ([jsc canSetOnBoxedObjects]) return false; // Setter fails AND WARNS if propertyName can't be set // This happens of non-JSCocoa ObjC objects, eg NSWorkspace.sharedWorspace.someVariable = value return throwException(ctx, exception, [NSString stringWithFormat:@"(in setter) object %@ does not support setting — Derive from that class to make it able to host any Javascript object ", privateObject.object]), false; } // // deleteProperty // delete property in hash // static bool jsCocoaObject_deleteProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyNameJS, JSValueRef* exception) { NSString* propertyName = (NSString*)JSStringCopyCFString(kCFAllocatorDefault, propertyNameJS); [NSMakeCollectable(propertyName) autorelease]; JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); if (![privateObject.type isEqualToString:@"@"]) return false; id callee = [privateObject object]; if (![callee respondsToSelector:@selector(setJSValue:forJSName:)]) return false; JSValueRefAndContextRef name = { JSValueMakeNull(ctx), ctx } ; name.value = JSValueMakeString(ctx, propertyNameJS); return [callee deleteJSValueForJSName:name]; } // // getPropertyNames // enumerate dictionary keys // static void jsCocoaObject_getPropertyNames(JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames) { JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(object); // If we have a dictionary, add keys from allKeys if ([privateObject.type isEqualToString:@"@"]) { id o = privateObject.object; // Vend property only for classes if (o == [o class]) { JSStringRef jsString = JSStringCreateWithUTF8CString(RuntimeInformationPropertyName); JSPropertyNameAccumulatorAddName(propertyNames, jsString); JSStringRelease(jsString); } if ([o isKindOfClass:[NSDictionary class]]) { id dictionary = privateObject.object; id keys = [dictionary allKeys]; for (id key in keys) { JSStringRef jsString = JSStringCreateWithUTF8CString([key UTF8String]); JSPropertyNameAccumulatorAddName(propertyNames, jsString); JSStringRelease(jsString); } } } } // // callAsFunction // done in two methods. // jsCocoaObject_callAsFunction is called first and handles // * C and ObjC calls : calls jsCocoaObject_callAsFunction_ffi // * Super call : in a derived ObjC class method, call this.Super(arguments) to call the parent method with jsCocoaObject_callAsFunction_ffi // * js function calls : on an ObjC class, use of pure js functions as methods // * toString, valueOf // // jsCocoaObject_callAsFunction_ffi calls a C function or an ObjC method with provided arguments. // // This uses libffi to call C and ObjC. static JSValueRef jsCocoaObject_callAsFunction_ffi(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, JSValueRef arguments[], JSValueRef* exception, NSString* superSelector, Class superSelectorClass, BOOL isVariadic, JSValueRef** argumentsToFree) { JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(function); JSCocoaPrivateObject* thisPrivateObject = JSObjectGetPrivate(thisObject); // Return an exception if calling on NULL if (thisPrivateObject.object == NULL && !privateObject.xml) return throwException(ctx, exception, @"jsCocoaObject_callAsFunction : call with null object"), NULL; // Function address void* callAddress = NULL; // Number of arguments of called method or function NSUInteger callAddressArgumentCount = 0; // Arguments encoding // Holds return value encoding as first element NSMutableArray* argumentEncodings = nil; // Calling ObjC ? If NO, we're calling C BOOL callingObjC = NO; // Structure return (objc_msgSend_stret) BOOL usingStret = NO; // Calling instance... , replaced with init... and released, making the js object sole owner BOOL callingInstance = NO; // Get delegate JSCocoaController* jsc = [JSCocoaController controllerFromContext:ctx]; id delegate = jsc.delegate; // // ObjC setup // id callee = NULL, methodName = NULL, functionName = NULL; // Calls can be made on boxed ObjC objects AND JSCocoaPrivateObjects if ([privateObject.type isEqualToString:@"method"] && ([thisPrivateObject.type isEqualToString:@"@"] || [thisPrivateObject.object class] == [JSCocoaPrivateObject class])) { callingObjC = YES; callee = [thisPrivateObject object]; methodName = superSelector ? superSelector : [NSMutableString stringWithString:privateObject.methodName]; // NSLog(@"calling %@.%@", callee, methodName); // // Delegate canCallMethod, callMethod // Called first so it gets a chance to do handle custom messages // if (delegate) { // Check if calling is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canCallMethod:ofObject:argumentCount:arguments:inContext:exception:)]) { BOOL canCall = [delegate JSCocoa:jsc canCallMethod:methodName ofObject:callee argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; if (!canCall) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling [%@ %@]", callee, methodName]); return NULL; } } // Check if delegate handles calling if ([delegate respondsToSelector:@selector(JSCocoa:callMethod:ofObject:privateObject:argumentCount:arguments:inContext:exception:)]) { JSValueRef delegateCall = [delegate JSCocoa:jsc callMethod:methodName ofObject:callee privateObject:thisPrivateObject argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; if (delegateCall) return delegateCall; } } // Special case for alloc autocall — do not retain alloced result as it might crash (eg [[NSLocale alloc] retain] fails in ObjC) if (![jsc useAutoCall] && argumentCount == 0 && [methodName isEqualToString:@"alloc"]) { id allocatedObject = [callee alloc]; // JSObjectRef jsObject = [JSCocoaController jsCocoaPrivateObjectInContext:ctx]; JSObjectRef jsObject = [jsc newPrivateObject]; JSCocoaPrivateObject* private = JSObjectGetPrivate(jsObject); private.type = @"@"; [private setObjectNoRetain:allocatedObject]; return jsObject; } // Instance call /* if ([callee class] == callee && [methodName isEqualToString:@"instance"]) { if (argumentCount > 1) return throwException(ctx, exception, @"Invalid argument count in instance call : must be 0 or 1"), NULL; return [callee instanceWithContext:ctx argumentCount:argumentCount arguments:arguments exception:exception]; } */ // Check selector if (![callee respondsToSelector:NSSelectorFromString(methodName)]) { // // Split call // set( { value : '5', forKey : 'hello' } ) // -> setValue:forKey: // if ([jsc useSplitCall]) { id splitMethodName = privateObject.methodName; id class = [callee class]; if (callee == class) class = objc_getMetaClass(object_getClassName(class)); BOOL isSplitCall = [JSCocoaController trySplitCall:&splitMethodName class:class argumentCount:&argumentCount arguments:&arguments ctx:ctx]; if (isSplitCall) { methodName = splitMethodName; // trySplitCall returned new arguments that we'll need to free later on *argumentsToFree = arguments; } } } // Get method pointer Method method = class_getInstanceMethod([callee class], NSSelectorFromString(methodName)); if (!method) method = class_getClassMethod([callee class], NSSelectorFromString(methodName)); // If we didn't find a method, try an instance call, then try treating object as Javascript string, then try Distant Object if (!method) { // Instance Check if ([methodName hasPrefix:@"instance"]) { id initMethodName = [NSString stringWithFormat:@"init%@", [methodName substringFromIndex:8]]; id class = [callee class]; method = class_getInstanceMethod(class, NSSelectorFromString(initMethodName)); methodName = initMethodName; callee = [class alloc]; callingInstance = YES; } if (!method) { // (First) Last chance before exception : try treating callee as a Javascript string if ([callee isKindOfClass:[NSString class]]) { id script = [NSString stringWithFormat:@"String.prototype.%@", methodName]; JSStringRef jsScript = JSStringCreateWithUTF8CString([script UTF8String]); JSValueRef result = JSEvaluateScript(ctx, jsScript, NULL, NULL, 1, NULL); JSStringRelease(jsScript); if (result && JSValueGetType(ctx, result) == kJSTypeObject) { JSStringRef string = JSStringCreateWithCFString((CFStringRef)callee); JSValueRef stringValue = JSValueMakeString(ctx, string); JSStringRelease(string); JSObjectRef functionObject = JSValueToObject(ctx, result, NULL); JSObjectRef jsThisObject = JSValueToObject(ctx, stringValue, NULL); JSValueRef r = JSObjectCallAsFunction(ctx, functionObject, jsThisObject, argumentCount, arguments, NULL); return r; } } // Last chance before exception : try calling DO JSValueRef res = [jsc JSCocoa:jsc callMethod:methodName ofObject:callee privateObject:thisPrivateObject argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; if (res) return res; return throwException(ctx, exception, [NSString stringWithFormat:@"jsCocoaObject_callAsFunction : method %@ of object %@ not found — remnant of a split call ?", methodName, [callee class]]), NULL; } } // Extract arguments const char* typeEncoding = method_getTypeEncoding(method); // NSLog(@"method %@ encoding=%s", methodName, typeEncoding); argumentEncodings = [JSCocoaController parseObjCMethodEncoding:typeEncoding]; if (!argumentEncodings) { return throwException(ctx, exception, [NSString stringWithFormat:@"jsCocoaObject_callAsFunction could not parse type encodings %s of [%@ %@]", [JSCocoa typeEncodingOfMethod:methodName class:[[callee class] description]], methodName, [callee class]]), NULL; } // Function arguments is all arguments minus return value and [instance, selector] params to objc_send callAddressArgumentCount = [argumentEncodings count]-3; // Get call address callAddress = getObjCCallAddress(argumentEncodings); } // // C setup // if (!callingObjC) { if (!privateObject.xml) return throwException(ctx, exception, @"jsCocoaObject_callAsFunction : no xml in object = nothing to call (Autocall problem ? To call argless objCobject.method(), remove the parens if autocall is ON)") , NULL; // NSLog(@"C encoding=%@", privateObject.xml); argumentEncodings = [JSCocoaController parseCFunctionEncoding:privateObject.xml functionName:&functionName]; // Grab symbol callAddress = dlsym(RTLD_DEFAULT, [functionName UTF8String]); if (!callAddress) return throwException(ctx, exception, [NSString stringWithFormat:@"Function %@ not found", functionName]), NULL; // Function arguments is all arguments minus return value callAddressArgumentCount = [argumentEncodings count]-1; // // Delegate canCallFunction // if (delegate) { // Check if calling is allowed if ([delegate respondsToSelector:@selector(JSCocoa:canCallFunction:argumentCount:arguments:inContext:exception:)]) { BOOL canCall = [delegate JSCocoa:jsc canCallFunction:functionName argumentCount:argumentCount arguments:arguments inContext:ctx exception:exception]; if (!canCall) { if (!*exception) throwException(ctx, exception, [NSString stringWithFormat:@"Delegate does not allow calling function %@", functionName]); return NULL; } } } } // // Variadic call ? // If argument count doesn't match descripted argument count, // we may have a variadic call // // Possibly account for a missing terminating NULL in ObjC variadic method // -> allows calling // [NSArray arrayWithObjects:'hello', 'world'] // instead of // [NSArray arrayWithObjects:'hello', 'world', null] // BOOL sugarCheckVariadic = NO; // Check if selector or method names matches a known variadic method. This may be a false positive ... if (isVariadic) { // ... so we check further. if (methodName) isVariadic = [[JSCocoaController controllerFromContext:ctx] isMethodVariadic:methodName class:[callee class]]; else isVariadic = [[JSCocoaController controllerFromContext:ctx] isFunctionVariadic:functionName]; // Bail if not variadic if (!isVariadic) { return throwException(ctx, exception, [NSString stringWithFormat:@"Bad argument count in %@ : expected %d, got %d", functionName ? functionName : methodName, callAddressArgumentCount, argumentCount]), NULL; } // Sugar check : if last object is not NULL, account for it if (isVariadic && callingObjC && argumentCount && !JSValueIsNull(ctx, arguments[argumentCount-1])) { // Will be tested during argument conversion sugarCheckVariadic = YES; argumentCount++; } } else { if (callAddressArgumentCount != argumentCount) { return throwException(ctx, exception, [NSString stringWithFormat:@"Bad argument count in %@ : expected %d, got %d", functionName ? functionName : methodName, callAddressArgumentCount, argumentCount]), NULL; } } // // ffi data // ffi_cif cif; ffi_type** args = NULL; void** values = NULL; char* selector; // super call struct objc_super _super; void* superPointer; // Total number of arguments to ffi_call NSUInteger effectiveArgumentCount = argumentCount + (callingObjC ? 2 : 0); if (effectiveArgumentCount > 0) { args = malloc(sizeof(ffi_type*)*effectiveArgumentCount); values = malloc(sizeof(void*)*effectiveArgumentCount); // If calling ObjC, setup instance and selector int i, idx = 0; if (callingObjC) { selector = (char*)NSSelectorFromString(methodName); args[0] = &ffi_type_pointer; args[1] = &ffi_type_pointer; values[0] = (void*)&callee; values[1] = (void*)&selector; idx = 2; // Super handling if (superSelector) { if (superSelectorClass == nil) return throwException(ctx, exception, [NSString stringWithFormat:@"Null superclass in %@", callee]), NULL; callAddress = objc_msgSendSuper; if (usingStret) callAddress = objc_msgSendSuper_stret; _super.receiver = callee; #if __LP64__ _super.super_class = superSelectorClass; //#elif TARGET_IPHONE_SIMULATOR || !TARGET_OS_IPHONE // _super.class = superSelectorClass; #else _super.super_class = superSelectorClass; #endif superPointer = &_super; values[0] = &superPointer; // NSLog(@"superClass=%@ (old=%@) (%@) function=%p", superSelectorClass, [callee superclass], [callee class], function); } } // Setup arguments, unboxing or converting data for (i=0; i= callAddressArgumentCount) { arg = [[JSCocoaFFIArgument alloc] init]; [arg setTypeEncoding:'@']; [arg autorelease]; } else arg = [argumentEncodings objectAtIndex:idx+1]; // Convert argument JSValueRef jsValue = sugarCheckVariadic && i == argumentCount-1 ? JSValueMakeNull(ctx) : arguments[i]; BOOL shouldConvert = YES; // Check type o modifiers if ([arg typeEncoding] == '^') { // If holding a JSCocoaOutArgument, allocate custom storage if (JSValueGetType(ctx, jsValue) == kJSTypeObject) { JSStringRef jsName = JSStringCreateWithUTF8CString("isOutArgument"); BOOL isOutArgument = JSValueToBoolean(ctx, JSObjectGetProperty(ctx, JSValueToObject(ctx, jsValue, NULL), jsName, NULL)); JSStringRelease(jsName); if (isOutArgument) { id unboxed = nil; [JSCocoaFFIArgument unboxJSValueRef:jsValue toObject:&unboxed inContext:ctx]; if (unboxed && [unboxed isKindOfClass:[JSCocoaOutArgument class]]) { if (![(JSCocoaOutArgument*)unboxed mateWithJSCocoaFFIArgument:arg]) return throwException(ctx, exception, [NSString stringWithFormat:@"Pointer argument %@ not handled", [arg pointerTypeEncoding]]), NULL; shouldConvert = NO; [arg setIsOutArgument:YES]; } if (unboxed && [unboxed isKindOfClass:[JSCocoaMemoryBuffer class]]) { JSCocoaMemoryBuffer* buffer = unboxed; [arg setTypeEncoding:[arg typeEncoding] withCustomStorage:[buffer pointerForIndex:0]]; shouldConvert = NO; [arg setIsOutArgument:YES]; } } } if (shouldConvert) { // Allocate default storage [arg allocateStorage]; } } args[idx] = [arg ffi_type]; if (shouldConvert) { BOOL converted = [arg fromJSValueRef:jsValue inContext:ctx]; if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"Argument %c not converted", [arg typeEncoding]]), NULL; } values[idx] = [arg storage]; } } // Get return value holder id returnValue = [argumentEncodings objectAtIndex:0]; // Allocate return value storage if it's a pointer if ([returnValue typeEncoding] == '^') [returnValue allocateStorage]; // Setup ffi ffi_status prep_status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (unsigned int)effectiveArgumentCount, [returnValue ffi_type], args); // // Call ! // if (prep_status == FFI_OK) { void* storage = [returnValue storage]; if ([returnValue ffi_type] == &ffi_type_void) storage = NULL; // Catch exceptions when calling ObjC if (callingObjC) { @try { ffi_call(&cif, callAddress, storage, values); } @catch (NSException* e) { if (effectiveArgumentCount > 0) { free(args); free(values); } [JSCocoaFFIArgument boxObject:e toJSValueRef:exception inContext:ctx]; return NULL; } } else ffi_call(&cif, callAddress, storage, values); } if (effectiveArgumentCount > 0) { free(args); free(values); } if (prep_status != FFI_OK) return throwException(ctx, exception, @"ffi_prep_cif failed"), NULL; // Return now if our function returns void // Return null as a JSValueRef to avoid crashing if ([returnValue ffi_type] == &ffi_type_void) return JSValueMakeNull(ctx); // Else, convert return value JSValueRef jsReturnValue = NULL; BOOL converted = [returnValue toJSValueRef:&jsReturnValue inContext:ctx]; if (!converted) return throwException(ctx, exception, [NSString stringWithFormat:@"Return value not converted in %@", methodName?methodName:functionName]), NULL; // Instance call : release object to make js object sole owner if (callingInstance) { JSCocoaPrivateObject* private = JSObjectGetPrivate(JSValueToObject(ctx, jsReturnValue, NULL)); [private.object release]; } return jsReturnValue; } // // This method handles // * C and ObjC calls // * Super call : retrieves the method name to call, thereby giving new arguments to jsCocoaObject_callAsFunction_ffi // * js function calls : on an ObjC class, use of pure js functions as methods // * toString, valueOf // static JSValueRef jsCocoaObject_callAsFunction(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { JSCocoaPrivateObject* privateObject = JSObjectGetPrivate(function); JSValueRef* superArguments = NULL; id superSelector = NULL; id superSelectorClass = NULL; // Pure JS functions for derived ObjC classes if ([privateObject jsValueRef]) { if ([privateObject.type isEqualToString:@"jsFunction"]) { JSObjectRef jsFunction = JSValueToObject(ctx, [privateObject jsValueRef], NULL); JSValueRef ret = JSObjectCallAsFunction(ctx, jsFunction, thisObject, argumentCount, arguments, exception); return ret; } else if ([privateObject.type isEqualToString:@"externalJSValueRef"]) { JSContextRef externalCtx = [privateObject ctx]; JSObjectRef jsFunction = JSValueToObject(externalCtx, [privateObject jsValueRef], NULL); if (!jsFunction) { throwException(ctx, exception, [NSString stringWithFormat:@"WebView call : value not a function"]); return JSValueMakeNull(ctx); } // Retrieve 'this' : either the global external object (window), or a result from previous calll JSObjectRef externalThisObject; JSCocoaPrivateObject* privateThis = JSObjectGetPrivate(thisObject); if ([privateThis jsValueRef]) externalThisObject = JSValueToObject(externalCtx, [privateThis jsValueRef], NULL); else externalThisObject = JSContextGetGlobalObject(externalCtx); if (!externalThisObject) { throwException(ctx, exception, [NSString stringWithFormat:@"WebView call : externalThisObject not found"]); return JSValueMakeNull(ctx); } // Convert arguments to WebView context JSValueRef* convertedArguments = NULL; if (argumentCount) convertedArguments = malloc(sizeof(JSValueRef)*argumentCount); for (int i=0; i", [self class], self, ((id)self == (id)[self class]) ? @"Class" : @"", [boxedObject class], boxedObject, retainCount]; } @end