Context: I'm working on a Pharo/Smalltalk -> Objective-C bridge
Scenario: In the following Objective-C ScriptingBridge snippet:
iTunesApplication *iTunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"];
iTunesTrack *currentTrack = iTunes.currentTrack; //[1]
// This low level way works too
//iTunesTrack *currentTrack = [iTunes propertyWithCode: 'pTrk']; //[2]
[iTunes playpause]; //[3]
Problem: The bridge uses class_getInstanceMethod
to determine if an object understands a message/selector, but it returns NULL for scripting messages like playpause
Question #1
Why does class_getInstanceMethod
return NULL for scripting messages like playpause
? Same question for class_copyMethodList
? What is special about scripting messages that they do not act like other Obj-C messages (except when they do!)?
Question #2 [SOLVED - see @Matt's answer]
Where, as per the docs, in the "dynamically defined subclass for the iTunes application" does SB put the "application-specific methods that handle the sending of Apple events automatically"? And, given that class_getInstanceMethod
fails to find this behavior (see below), what's a reliable way for the bridge to test for it (i.e. whether such a method/message exists)?
The Objective-C Runtime API reports mixed results. On one hand, the iTunesApplication
class seems not to have any methods (or properties for that matter):
class_copyMethodList([iTunes class]...
returns zero methodsclass_getInstanceMethod
, which the bridge uses to find and execute methods, fails.On the other, #playpause
can be queried and sent through other parts of the API:
respondsToSelector:
-> TRUEmethodSignatureForSelector:
returns a signature performSelector:
actually sends the message Strangely, methodForSelector:@"playpause"
successfully returns an IMP
in Obj-C, but crashes if sent from the other side of the bridge.
Question #3 [SOLVED]
How one would simulate/replicate [3]?
Answered by @Willeke in comments: [iTunes sendEvent:'hook' id:'PlPs' parameters:0]
If SB doesn't use Objective-C messages, what do the docs mean by "subclasses of SBApplication implement application-specific methods that handle the sending of Apple events automatically"? Why does iTunes respondsToSelector: @"playpause" work i.e. return true? And how does [iTunes playpause] work? Etc, etc..
It works because the very first thing you do in a scripting bridge application is generate a header. In Catalina you do it like this:
sdp -f h --basename iTunes /System/Applications/Music.app/Contents/Resources/com.apple.Music.sdef
This reads the iTunes dictionary (sdef
) and generates the header for a group of analogous Objective-C classes. Now you have an iTunes.h file that you include in the application project and import in your code. It contains this line:
- (void) playpause; // toggle the playing/paused state of the current track
So now playpause
is declared, explicitly, as a legal command that you can send to an iTunesApplication object. Then, when you actually run your application, you say
iTunesApplication* tunes = (iTunesApplication*)[SBApplication applicationWithBundleIdentifier:@"com.apple.music"];
This causes your application to talk to iTunes (Music) and get the dictionary (sdef
) again, generating the implementation for the methods declared in the header. The implementation for the playpause
command is exactly what the sdef
says it should be: namely, to send the hookPlPs
event to iTunes.
So that explains both why you are allowed to say playpause
and what happens when you say it.
That is what AppleScript is — it's an application supplying a list of things you can say to it using Apple events, along with English-like terms that reference those Apple events.
So if you want to write a bridge, you have to do the same thing: you need to provide a way to scour the sdef
resource of a target application and translate that information into a way for corresponding commands to be given in your language, whatever it is.