Home > Blogs > Ask Big Nerd Ranch: Adding Python Scripting to Cocoa apps

Ask Big Nerd Ranch: Adding Python Scripting to Cocoa apps

By  Mar 26, 2010

Topics: Programming

Q: How would one go about writing a Python plugin system for a Mac application written in Cocoa?

A: Implementing a plugin system of any kind consists of exposing some internal data or functions of an application to an external piece of executable code, which in this case is a Python script. Doing so allows for users to extend an application's functionality beyond what the application's author intended or conceived. We'll take a look at how to execute Python scripts from within a Cocoa app, and we'll discuss how to structure an application to allow for flexible scriptability.

Plugin systems come in many shapes and sizes, and which type of system you choose depends largely on the desired goals you wish to achieve, and on the type of application you're writing. Some plugins are designed to transform a user's data. For instance, a graphics-editing application may allow users to add plugins that manipulate the current image, perhaps using the Core Image framework. These types of plugins are typically triggered on-demand by the user, often by clicking a menu item in a Scripts menu. Others plugins are designed to add entirely new tools to an application. A graphics editor may allow users to create tool palettes of their own invention, and add them to the application's user interface as a plugin. Typically, this kind of plugin is running the entire time the host application runs, and its actions are invoked by a user's interaction with graphical controls (often times provided by the plugin itself).

In this article, we are going to take the first approach to create a simple, yet flexible, plugin architecture. We will start with a bare-bones text editing application, and add a Scripts menu, which will dynamically list the scripts inside a pre-specified folder. When one of these script menu item is chosen by the user, the script will be loaded into the application's memory, and given a chance to manipulate the current document's text object. The function that we will require all of our application's Python scripts to implement will be called "main" and take one argument, an NSTextView object. This way, scripters will have a very large amount of control over the contents of an open document in our application.

I've created a sample application for the purpose of materializing our hypothetical sample code. The application is called ScriptableTextEditor, feel free to download the full, open-source project if you want to follow along.

Before we get started, it's important to note that I haven't done any special preparations to this project before attempting to add scriptability; I haven't set any secret compiler flags or hidden environment variables. Any existing Cocoa application can be made scriptable with Python using these instructions.

The first step to actually creating a Python scripting system in Cocoa is to add Python.framework to the project, preferably under the Linked Frameworks subgroup in your Xcode project. This loads a Python library that lets us run interpreted code from inside Objective-C.

Next we need to create a Python script file that will actually do our dirty work for us. We're going to execute this file when the application launches. Then, when a user tries to execute a Python script, we'll bounce the call from our Objective-C code into this Python class, and actually load the Python script from here. In the sample project, this "bouncer" class is STEPluginExecutor, found inside STEPluginExecutor.py. It's important to note that it derives from NSObject, because this allows us to talk to the STEPluginExecutor class natively from within our Objective-C code.

Technically this step is optional, and we could instead just execute each individual Python script from Objective-C any time the user chooses one. However, using a "bouncer" script inside our application has several benefits. For one thing, each script will need to import Foundation and AppKit, which can take up to a few second even on very speedy computers. If we instead import the Python script from our bouncer script, we only need to load Foundation and AppKit once, thus speeding up script execution and improving the user experience. Secondly, due to the concise nature of Python and verbose nature of the C library for Python, it would be a lot more work to execute each Python script from Objective-C directly. Plus, Python makes this task very easy for us (as usual) by natively supplying us the "imp" module, which allows us to load a module from any given Python script, as if we had directly used the "import" keyword to load it.

The implementation of our STEPluginExecutor class is simple: there is one class method, which takes a full script path, a function name, and an array of arguments as its parameters. (In our case, the second argument will always be "main" and the third will always be an array containing a single object, our NSTextView.) This class method attempts to load the script, and call the aforementioned function and array of arguments. If an error occurs, we run a modal alert panel describing the error to the user, in hopes they will report it to the script author. The STEPluginExecutor class is simple, but suitable for our needs; the way it's structured allows us to greatly extend our scripting capabilities in the future, without needing to change the implementation of this "bouncer" class.

Now that we have defined the STEPluginExecutor class, we need to load it into memory. For the sake of cleanliness, I've created a singleton class that handles all scripting-related functionality for us, called STEPluginManager, found inside STEPluginManager.m. This class has two methods, -setupPythonEnvironment which is only called once when our application finishes launching, and -loadScriptAtPath:runFunction:withArguments: which simply passes its argument onto our bouncer class. Let's look at the setup method first.

Setting up a Python runtime inside a Cocoa app is surprisingly easy. There are less than a handful of functions to call, and then we're all set. Before we call the Python library, we will need to add #import <Python/Python.h> to the top of our file. This way, the following functions will not generate compiler warnings. The first call we make is Py_SetProgramName("/usr/bin/python"), and only as a precaution. (If we don't set this variable, the default value of "python" may not be sufficient to find the Python executable, if a user has modified their environment path. It's a safe bet to call this function first.) Next, we call Py_Initialize(), which takes no arguments. At this point, we have a working runtime inside our app! Finally, we need to use the PyRun_SimpleFile() macro to load our bouncer script into memory and execute it. This macro takes 2 arguments, a file object opened with fopen(), and a C string of the full script path. This is a rough equivalent of what our method will look like in the end:


- (BOOL) setupPythonEnvironment {
Py_SetProgramName("/usr/bin/python");
Py_Initialize();

NSBundle *bundle = [NSBundle mainBundle];
NSString *scriptPath = [bundle pathForResource:@"STEPluginExecutor"
ofType:@"py"];

char* script_path = (char*)[scriptPath UTF8String];
char* script_name = basename(script_path);

FILE *mainFile = fopen(script_path, "r");
return (PyRun_SimpleFile(mainFile, script_name) == 0);
}

At this point, we have completed all the steps to enable our application to execute scripts! Now we simply need to connect the dots, so that a user's action in the user interface will trigger a call to our STEPluginManager singleton class. Since this is pretty standard Cocoa at this point, I'll be brief: our application-delegate object in STEAppDelegate.m moonlights as the delegate for our Scripts menu. Whenever the menu is opened, the STEAppDelegate object is told to repopulate the menu. It searches the scripts folder (currently just the Desktop for simplicity of demonstration) for Python script files, ending with the .py extension. For each found script, a menu item is added, containing the full file path, and using the selector -runScript:. Our NSDocument subclass, STETextDocument (inside STETextDocument.m), implements the -runScript: method, which retrieves the script's path, and passes it along to the singleton with the function name "main" and the NSTextView as the only argument.

Voila, we now have a basic text editor Cocoa application, with a working Python scripting interface! Copy the Python script files from the sample application's Scripts folder onto your Desktop. Run the application, type in a few lines of text, and click some of the items in your Scripts menu. Feel free to edit any of these scripts and run them again, all while the application is still running!

It is very important to note that the plugin system we have discussed here is intended for demonstration purposes only. Production applications typically do not expose Cocoa controls (such as NSTextView and NSTextStorage) through the plugin system directly. Depending on the context of the application, exposing these details can confuse scripters, because most scripters will not be familiar with Objective-C or Cocoa. Instead, a well-conceived plugin system should hide the implementation details of an application. Often times, the plugin system will instead create "wrapper" functionality around the internal implementation of the application, so potential scripters will only need to learn the handful of new functions and data types in your own simplified API, rather than learning an entire, comprehensive API, such as AppKit.

Loading the Python runtime in a Cocoa application is simpler than it may first seem; using only a few lines of code, any Python script can be loaded into memory and executed dynamically. With a little extra forethought put into the structure of our application, we have created a simple scripting system that enables our users to enjoy tying the simplicity, power, and elegance of Python, to the comprehensive functionality of a native Cocoa application.

Become an InformIT Member

Take advantage of special member promotions, everyday discounts, quick access to saved content, and more! Join Today.

Other Things You Might Like

Xamarin Unleashed

Xamarin Unleashed