A few years ago, I explored the idea of writing a small interpreter for embedding into McLaren Labs applications. The concept was to extend rtpmidi
or a synth
application with a scriptable ability to customize controls, behaviors or sound graphs. This would help make each application very slim, but to allow extension through scripting. (This is not a very novel idea: it’s been done many times before. 🙂 )
The Objective-C execution environment (libobjc) provides many hooks that make it fairly easy to call into the ObjC runtime from an interpreter, or even to bridge Objc method calls back to an interpreter. The idea of a scritping environment to accompany a GNUstep ObjC Appplication program seemed promising and I wrote a little interpreter (based on PostScript syntax) to test out some of the ideas.
Along the way, I learned about a much more mature project in libs-steptalk
. Among other things, it provides a SmallTalk-like language for extending applications or even gluing them together in the GNUstep workspace. It seemed very old, but there were some tremendous ideas in there: I wondered if I could learn enough about it to use it, and I wondered if it still worked.
(Spoiler Alert! I did and it does.)
gs-desktop
The gs-desktop project (released in 2022?) is embracing StepTalk as a first class component. Some of the applications provided are somewhat thin wrappers around a lower-level command line utility … but augmented with scripting.
Take a look at the ScreenShot application. There is a category called ApplicationController+scripting.m
that provides three methods:
- grabScreen
- grabWindow
- grabSelection
These methods may be called by any other program in the desktop through the scripting environment. A good example is the Script called ScreenShotScreen in the /Scripts
folder of the desktop. It is a shell program that starts the StepTalk interpreter (stexec
) and invokes the method of the ScreenShot application.
StepTalk is the glue that binds together the applications of the desktop to allow customization and extension.
A “script” that gives a glimpse of the power of StepTalk is also included here: NSTextField. This script is an entire program that creates AppKit
widgets programmatically through StepTalk to
- create an Application
- create a Panel with a title
- put two buttons on the Panel (“Ok” and “Cancel”)
- return the value associated with the button pressed
Take a look at the source if you’re intrigued.
Note: I believe the onflapp
version of StepTalk has a few additions that make the execution of the intepreter in a script seamless with the #!
syntax. These changes have not yet been accepted upstream.
Making your application extensible via scripts
The discussion above focused on how to make an application “callable” externally through scripting. There is another use case where you want your application to be extensible by scripts – either provided by yourself or your customers.
There is good support for this too. By following a few conventions and actually not writing that much code, your Application will have a new top-level menu called “Scripting”. There will be the following items:
- Scripts panel …
- Transcript …
- Do Selection
- Do & Show Selection
The first one is very interesting: it will reveal a panel that lists all of the Scripts findable for your application. The search path includes
- /StepTalk/Scripts/.
- /StepTalk/Scripts/Shared
- paths to Resource/Scripts in all loaded bundles including the main bundle
The second menu item opens up the Transcript window where scripts can write output with the showLine:
method. The creation of these menu items, the scripts Panel and the Transcript window are all part of libs-steptalk
, and applications using them need to do very little.
Building a little scriptable application
The first thing to do is to decide what objects or classes you would like to expose to scripting in your application.
The next thing is to understand a few of the main pieces of the libs-steptalk
project layout. A sketch is shown below.
libs-steptalk/
ApplicationScripting/
Source/
Support/
Frameworks/
StepTalk
Modules/
Languages/
Smalltalk
The Frameworks/StepTalk
directory contains the core of the StepTalk environment. It provides language-independent support for iterpreters that interact with an Objective-C environment.
The Languages/Smalltalk
directory contains a parser and language support for the Smalltalk dialect. It runs on top of the StepTalk framework.
The Modules
directory defines collections of methods and objects that can be loaded into the StepTalk environment. There are modules for Foundation
, AppKit
and some other things. The StepTalk interpreter environment can be extended with new functionality through additional modules.
The ApplicationScripting
directory contains elements of the “glue” that help link StepTalk into your application. The ApplicationScripting/Source
directory contains code that builds the extension menu shown above as well as the Transcript window. You shouldn’t need to touch any of the code in this directory.
The ApplicationScripting/Support
directory contains code that you must COPY into your project. There are three files.
- ScriptingInfo.plist
- STScriptingSupport.h
- STScriptingSupport.m
You should edit ScriptingInfo.plist
to include the names of the objects, classes and methods you want your application to expose in the scripting environment. You must include it as a resource in the GNUmakefile. The way this .plist
file is used is that at runtime, the StepTalk framework looks for the application bundle, and finds ScriptingInfo.plist
. It uses this information to add objects, classes and methods into the StepTalk interpreter namespace.
The next two files are almost used “as-is”. But there is a caveat: there are three methods that must be commented out.
#if 0
- (void)orderFrontScriptsPanel:(id)sender
{
[self _loadAppTalkAndRetryAction:_cmd with:sender];
}
- (void)orderFrontTranscriptWindow:(id)sender
{
[self _loadAppTalkAndRetryAction:_cmd with:sender];
}
- (NSMenu *)scriptingMenu
{
return [self _loadAppTalkAndRetryAction:_cmd];
}
#endif
These three methods do something that might have worked at one point in time, but do not with todays’ compilers and linkers (at least on Linux). They each attempt to dynamically load a bundle that adds a category to NSApplication
and then call the category method.
What we have found works better is to explicitly load the bundle (ApplicationScripting) and then just call the category method. We’ll show how a little later.
A tiny application with scripting
To demonstrate these ideas, we wrote a tiny program with scripting support. In our ApplicationDelegate we create a window, and attach two buttons and a text view for output. There is no other behavior defined. The buttons are properties on the AppDelegate called but1
and but2
and the textview is text1
.
To load scripting support, I recommend putting the following in the launch method.
- (void) applicationDidFinishLaunching: (NSNotification *) aNotif
{
if ([NSApp isScriptingSupported]) {
[NSApp initializeApplicationScripting];
}
...
To add the scripting menu to your menuBar, you need to add the following.
if([NSApp isScriptingSupported])
{
id<NSMenuItem> scriptingItem =
[self.menuBar addItemWithTitle: @"Scripting"
action: NULL
keyEquivalent: @""];
if ([NSApp respondsToSelector:@selector(scriptingMenu)]) {
NSMenu *menu = [NSApp scriptingMenu];
[scriptingItem setSubmenu: menu];
}
}
That’s it for scripting support.
Then we wrote a script, called ‘script1.st’ and put it in a directory where it would be found. USing the Scripting >> Scripts Panel ...
menu item, we load script1. The script appears below.
"StepTalk file named script1.st"
[| :global
main
| local |
"NOTE: button targets are not retained, and "
"we need the scriptObject to be."
currentScript := self.
but1 setTarget:self.
but1 setAction:#doAction:.
but2 setTarget:self.
but2 setAction:#doAction:.
^self
!
doAction:sender
sender == but1
ifTrue: [
text1 setStringValue:'But 1 pressed'.
].
sender == but2
ifTrue: [
text1 setStringValue:'But 2 pressed'.
].
^self
]
This script registers a callback on each button to a script method called doAction:
. The action determines which button was pressed and writes an output into the text field. Voila! A scriptable application.
A Warning
The methods in the script are methods of the “script object.” The script object is an object all on its own, and it is subject to normal retain/release operations, or ARC. The target
property of an NSControl
is interesting — it does not retain its target. Thus, since we need the script object to be retained to call its methods, we need to find some way to retain it. This could be done by assigining it to a property somewhere in Objective-C, or doing what we did here: assigning it to a variable in the Scripting Environment.
Conclusion
StepTalk is pretty cool, and it is a solid piece of work that deserves some attention. Recently, there has been renewed interest in GnuStep desktop – with the “gs-desktop” project, another one called “Agora” (https://github.com/AgoraDesktop) that both aim to make all of these things easier to install. It’s worth checking out – the classes and objects of the applications and desktop are all interoperable. It’s a desktop environment that a programmer can love.
Postscript
To our readers: We’ll share the tiny ButtonDemo program in https://github.com/mclarenlabs so that it’s easy for you to try out.