Javascript does Cocoa too


Running Objective-C code from Javascript

Java, Python and Ruby can access Cocoa APIs and Objective-C classes. What about the super-extensible yet severely under-appreciated Javascript? With the WebKit framework, you can access Objective-C from any script present in a HTML document.

A `WebScriptObject` is an Objective-C wrapper passed to your application from the scripting environment. You can’t create an instance directly, but you need to get a `WebScriptObject` by sending `webScriptObject` to a WebView.

Paraphrase of WebScriptObject Overview

This class, WebScriptObject, lets you “talk” and “listen” to Javascript (or any other code allowable inside HTML <script> tags). That is, you can run Javascript code or functions in the WebView, but you can also let the Javascript run Objective-C methods.

This is the same way Dashboard widgets interact with Cocoa plugins. You know how you access window.widget in your Javascript? That’s an Objective-C class you’re referencing. The WebKit framework provides all the necessary classes and functionality for this interaction, making this seemingly difficult task ridiculously easy.

Exposing an Objective-C class to the scripting environment

Exposing a class to the scripting environment is very easy. You can use key-value coding methods (for example, setValue:forKey: and valueForKey:) to get and set the properties of a WebScriptObject. These will be accessible as children of the window object in Javascript. Use the removeWebScriptKey: method to remove a scripting object property.

As an example, let’s say we have a class NSFoo. We’ve linked to the WebKit framework and webView is a WebView.

We use the delegate method webView:windowScriptObjectAvailable: to ensure that the WebScriptObject is available before we start using it. Then we expose an instance of NSFoo to the scripting environment:

- (void)webView:(WebView *)sender windowScriptObjectAvailable: (WebScriptObject *)windowScriptObject {
    NSFoo *myFoo = [[NSFoo alloc] init];

    scriptObject = windowScriptObject;
    [scriptObject setValue:myFoo forKey:@"foo"];
}

Now you can access foo through javascript;

<script language='text/javascript'>
var myFoo = foo;
</script>

Allow Javascript access

For obvious security reasons, no methods or KVC keys are exposed to the Javascript environment by default. This means at this stage while window.foo exists, you can’t do much with it. To allow Javascript to send messages or get / set properties, the following two class methods need to be utilized:

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector;
+ (BOOL)isKeyExcludedFromWebScript:(const char *)name;

So for example, if we want to allow Javascript to send a bar: message to a class, all we have to do is add the following class method to the implementation:

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector {
    if ( selector == @selector(bar:) ) {
        return NO;
    }
    return YES;
}

Now running foo.bar_(0) in Javascript will be equivalent to running [myFoo bar:0] in Objective-C. Why the underscore? Selectors accessed from Javascript are renamed using the following rules:

  • Any colon (":") in the Objective-C selector is replaced by an underscore ("_").
  • Any underscore in the Objective-C selector is prefixed with a dollar sign ("$").
  • Any dollar sign in the Objective-C selector is prefixed with another dollar sign.

So a method called doAction:(id)action withThing:(id)thing will be turned into doAction_withThing_(action,thing), while do$This: will become do$$This_. This is often pretty ugly and inconvenient. Fortunately, the Javascript name for the selectors can be changed.

Change Javascript selector names

Using + (NSString *)webScriptNameForSelector:(SEL)sel, we can change the name of the selector in Javascript:

+ (NSString *)webScriptNameForSelector:(SEL)sel
{
    if (sel == @selector(bar:))
        return @"bar";
    return nil;
}

Now we can run foo.bar(0) in Javascript to equal [myFoo bar:0]. Here are a few more examples:

+ (NSString *)webScriptNameForSelector:(SEL)sel
{
    if (sel == @selector(performTransition))
        return @"performTransition";    // widget.performTransition()
    else if (sel == @selector(prepareForTransition:))
        return @"prepareForTransition"; // widget.prepareForTransition(arg)
    else if (sel == @selector(setPreference:forKey:))
        return @"setPreferenceForKey";  // widget.setPreferenceForKey(pref,key)
    else if (sel == @selector(preferenceForKey:))
        return @"preferenceForKey";     // widget.preferenceForKey(key)
    return nil;
}

If you want to expose all the instance methods of the class to the scripting environment, just return NO without doing any checks:

+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector { return NO; }

Remember: selectors are converted into Javascript functions, so even if the method doesn’t need any arguments, you still need to add the parenthesis at the end for the Javascript; eg. [object doMethod] becomes object.doMethod().

Key-Value-Coding from Javascript

Exposing KVC keys is a similar procedure. Here we’re giving access to the “name” property:

+ (BOOL)isKeyExcludedFromWebScript:(const char *)property {
    if (strcmp(property, "name") == 0) {
        return NO;
    }
    return YES;
}

Now in Javascript, we can read and write foo.name. Reading foo.name will be equivalent to [myFoo name] and writing to it (foo.name = "A big foo") will be the same as running [myFoo setName:@"A big foo"].

As before, just return NO without any checks to allow all KVC keys to be accessed from the scripting environment.

Summary

  • Load WebKit.framework which contains the classes WebView and WebScriptObject.
  • Send setValue:forKey: to a WebScriptObject; makes an object available to the scripting environment.
  • Implement +isSelectorExcludedFromWebScript: and +isKeyExcludedFromWebScript: and return NO to allow the scripting environment to run a method or access a key.
  • Use webScriptNameForSelector: / webScriptNameForKey: to specify the names of your methods / keys as seen by Javascript.

Further reading


Back to Top ↑

8 Comments so far

Leave a comment
  1. 1

    “Implement +isSelectorExcludedFromWebScript: and +isKeyExcludedFromWebScript: and return YES to allow the scripting environment to run a method or access a key.”

    Or return NO. NO is also good. ;)

  2. 2

    Umm… Oops! There’s always one mistake.

    Thanks for picking that up.

  3. 3

    Ankur - Didn’t know that we could access Code APIs using JavaScript framework.

    Would try out the code.

  4. 4

    Just out of interest should it be sel == @selector(performTransition:) - you seem to missing a colon, not tested, just skim reading :)

  5. 5

    should it be sel == @selector(performTransition:)

    Colon would be needed if widget.performTransition() took arguments, which it doesn’t. (So it’s actually not necessary to provide a name for it.)

RSS feed for comments on this post. TrackBack URI

Leave a comment

Comments may be edited for formatting.