4

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 methods
  • class_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: -> TRUE
  • methodSignatureForSelector: returns a signature
  • and 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]

Sean DeNigris
  • 6,306
  • 1
  • 31
  • 37
  • 1
    I'm not sure I understand the question. The "low level way" of `[iTunes playpause]` is `[iTunes sendEvent:'hook' id:'PlPs' parameters:0]`. `[iTunes propertyWithCode: 'pTrk']` returns a `SBObject*`, `[iTunes propertyWithClass:[self.iTunes classForScriptingClass:@"track"] code:'pTrk']` returns a `iTunesTrack*`. – Willeke May 26 '20 at 09:54
  • @Willeke that is already helpful in case I can't find an answer. My question boils down to: how do I access e.g. "the currentTrack method of the dynamically defined subclass for the iTunes application" from the other side of the bridge if the Obj-C runtime reflection API can't seem to see that method? Where does SB "hide" these dynamically implemented methods/properties? I'd rather somehow call "playpause" from Smalltalk rather than reimplement it with code similar to what you provided – Sean DeNigris May 26 '20 at 23:01
  • The "application-specific methods" is referring to the scripting terms that are being used in your application. These terms aren't the kind of methods you are looking for, they are the commands (via Apple Events) that an application responds to. You can try compiling/running the script, and catch if there is an error (syntax or other), or look at an application's scripting dictionary (although that still won't tell if a term is being used correctly). – red_menace May 29 '20 at 12:50
  • @red_menace Are you saying that there are no actual methods, but that `SBApplication` is stimulating methods by dynamically intercepting ` respondsToSelector:` and friends and converting message sends into Apple Event sends? – Sean DeNigris May 30 '20 at 13:39
  • 1
    Why not study an alternative open source bridge to see how it’s done? Look at appscript etc. Warning, here be dragons. If this was easy others would have done it. – matt May 30 '20 at 13:40
  • There would obviously be methods to implement the various Apple Events, but it would just be coincidental if a method exists with the same name as a scripting term. There isn't a one-to-one match of an application's scripting terminology and methods in the target application, but you can't just call some random method in another application anyway - see the [Cocoa Scripting Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_intro/SAppsIntro.html#//apple_ref/doc/uid/TP40002164) for more information. – red_menace May 30 '20 at 19:38
  • @matt, I must be explaining poorly! Used Applescript for 12yrs; learned much from ur book :) Love scripting, but hate AS. appscript was a dream come true, until it died. Now SB might work for my use cases. Enter the Pharo/Smalltalk Obj-C bridge above. I want to use SB via this bridge to communicate with scriptable applications from within Pharo. However, Pharo bridge is not able to directly find/send SB-created messages that mirror script commands/properties because it internally uses `class_getInstanceMethod`, which fails for these SB methods. Thus, I'm searching for a workaround... – Sean DeNigris Jun 01 '20 at 02:16
  • 1
    Ah, well, sorry, I know nothing about Pharo or `class_getInstanceMethod`. But appscript is open source so, as I say, if you want to know how it works, you can see for yourself. That's all I have to offer, sorry. – matt Jun 01 '20 at 02:18
  • @red_menace Isn't exactly much of the point of ScriptingBridge that it provides Obj-C messages that are a 1-to-1 correspondence to scripting terms?! The example sending `playhouse` was from the SB doc, not something I made up (and works in Obj-C, just not in Pharo without some Obj-C runtime magic). – Sean DeNigris Jun 01 '20 at 02:18
  • @matt Thanks. I'll see if appscript can provide some inspiration. – Sean DeNigris Jun 01 '20 at 02:19
  • ScriptingBridge doesn't work like that - it bridges Cocoa objects and data types, but the mechanism used is _Apple Events_, not Objective-C messages. The target application receives and sends these Apple Events, implementing them via its scripting interface, which is what defines the terms. – red_menace Jun 01 '20 at 12:45
  • @red_menace I understand that ultimately Apple Events are sent to the target application. What I'm missing is what happens between `[iTunes playpause]` and the Apple event. 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... – Sean DeNigris Jun 03 '20 at 03:36
  • 1
    Because you start by generating, physically, an iTunes class based on the iTunes dictionary (sdef), in which `playpause` is declared. And it is an SBApplication subclass. Gosh, I’m sorry now that I deleted my earlier comment. – matt Jun 03 '20 at 11:59

1 Answers1

5

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I very much appreciate your continued dialog on this. I'm tempted to accept this answer because it covers so much of my question and I suspect its the best I'm going to get. But, while I investigate appscript, I amended the question to focus on the main part that's still missing i.e. What is special about SB scripting messages that they do not act like other Obj-C messages (except when they do!)? For example, why do Obj-C runtime functions like `class_getInstanceMethod` and `class_copyMethodList` fail to "see" scripting messages like `playpause`? – Sean DeNigris Jun 04 '20 at 14:11
  • I don't know enough about the runtime functions to answer that question. I think it has to do with the fact that a dynamically generated class like iTunesApplication is not linked into the app. But I don't see how that's relevant; those sort of runtime functions was _never_ how you were going to do this. It's not how _any_ AppleScript bridge works. That is my point. – matt Jun 04 '20 at 14:45
  • Okay, let me dig into the appscript code and see if I can resolve this based on your generous pointers... – Sean DeNigris Jun 04 '20 at 16:23
  • Well, it depends on the flavor of appscript. There's a Swift flavor that works exactly as I just described: you start by explicitly generating a Swift code file based on the target app's `sdef`, which is then built into the project. My SyncMe3 app uses that to script the Finder. – matt Jun 04 '20 at 16:40
  • On the other hand, rb-appscript, the ruby flavor, doesn't need to do that, because ruby uses duck typing: the compiler will let you say _anything_ to _any_ object and it can be resolved at runtime, so there is no need for a prior code file generation in order to be compilable later. We start instead at the second step, where our _code_ reaches out to the target app and obtains its `sdef` dynamically and interprets it, and uses the result to respond to `method_missing`. – matt Jun 04 '20 at 16:42
  • In other words, some languages can't compile without pre-generated "glue" code: Perl, Objective-C, Swift are common examples. Other languages are more dynamic and will let you say anything: JavaScript, Python, Ruby. – matt Jun 04 '20 at 16:45