Thomas Tempelmann
Thomas Tempelmann

Reputation: 12095

Cocoa Scripting: Intercept any Object-first message calls

I am writing my own non-ObjC framework around Cocoa Scripting (think of writing a scriptable Mac app in C, C++ or Java, or in my case, Xojo).

I like to be able to intercept any Object-first method invocation instead of having to add the actual method to the ObjC class (I can't because the framework won't know which message the app code can handle in advance - so it'll instead have to receive and pass on any command message once they come in from the scripting engine).

For instance, any property getters and setters can be intercepted via implementing

-valueForUndefinedKey:
-setValue:forUndefinedKey:

as well as all the methods of the NSScriptKeyValueCoding protocol.

I am looking for a similar way to intercept NSCommandScript messages sent to the method specified in these sdef elements:

<responds-to command="reload">
    <cocoa method="reloadList:"/>
</responds-to>

So, instead of implementing reloadList: by adding it to the class methods, I wonder if there's a generic way to catch all such calls.

I found that the class method

+ (BOOL)resolveInstanceMethod:(SEL)sel

gets invoked asking for reloadList:. But the same method is invoked for many other purposes as well, and so I rather not blindly intercept every such call because it would cause a rather severe performance hit if I'd forward them all to a Java function that tells me whether it wants to handle it, for instance.

I hope there's something that lets me tell that this selector is related to a NSScriptCommand before forwarding it further.

Upvotes: 1

Views: 247

Answers (2)

Thomas Tempelmann
Thomas Tempelmann

Reputation: 12095

After setting a breakpoint into the designated command handling method, I saw the following stack trace:

#0  0x00000001000197db in -[SKTRectangle rotate:]
#1  0x00007fff8ee0b7bc in __invoking___ ()
#2  0x00007fff8ee0b612 in -[NSInvocation invoke] ()
#3  0x00007fff8eeab5c6 in -[NSInvocation invokeWithTarget:] ()
#4  0x00007fff8b82cbde in -[NSScriptCommand _sendToRemainingReceivers] ()
#5  0x00007fff8b82cf39 in -[NSScriptCommand executeCommand] ()

This shows that NSScriptCommand does not appear to use any customizable special forwarding mechanism but uses NSInvocation to call the method.

Such invocations can be intercepted like this:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // Look for signatures with exactly one ":"
    if ([[NSStringFromSelector(aSelector) componentsSeparatedByString:@":"] count] == 2) {
        return [NSMethodSignature signatureWithObjCTypes:"@:@@"];
    } else {
        return [super methodSignatureForSelector:aSelector];
    }
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    id arg; // The first and only argument is expected to be of type NSScriptCommand*
    [anInvocation getArgument:&arg atIndex:2];
    if ([arg isKindOfClass:[NSScriptCommand class]]) {
        NSLog(@"executing the command...");
        // when finished, set the return value (which shall be an NSObject)
        id result = nil;
        [anInvocation setReturnValue:&result];
    } else {
        // oops - we cannot handle this
        [super forwardInvocation:anInvocation];
    }
}

This form of interception works better than using resolveInstanceMethod: because it doesn't get called so often but only for specific purposes such as an NSScriptCommand execution.

The problem with this, however, is that if other code also uses NSInvocation to make calls into the same class for other purposes, and if those calls use a matching selector signature, the above code would intercept those calls and then not handle them, possibly leading to unexpected behavior.

As long as the classes are known to be used only by the scripting engine and have no other behavior (i.e. they're immediate subclasses of NSObject), there is no reason for this to happen. So, in my special case where the classes act only as proxies into another environment, this may be a viable solution.

Upvotes: 1

foo
foo

Reputation: 3259

If it's not a Cocoa-based app then you're probably best to forget about using Cocoa Scripting as it's heavily coupled to the rest of the Cocoa architecture, install your own AE handlers directly using NSAppleEventManager and write your own View-Controller glue between those and whatever you eventually implement your Model in. See also: Scriptability (AppleScript) in a Mac Carbon application

ETA: Come to think of it, you might want to rummage around the web and see if you can dredge up any old C++ AEOM frameworks, as ISTR there were one or two around back in the pre-OS X days. May require some updating, and may or may not be any good (but then CS is rather crappy too), but it'd be far easier than starting completely from scratch as designing and implementing a good, robust, idiomatic (or even simplified) AEOM framework is a giant PITA, even when you do know what you're doing (and hardly anyone does).

Upvotes: 0

Related Questions