Ads1

Friday, May 25, 2012

iPhone Applications in Python

Writing iPhone applications is easy. Well, if you are willing to wait until Apple provides the ability to run their SDK-based applications on actual hardware. Until then, you are forced to go through a quite long list of steps. While I've updated this process somewhat, it's still anything but "easy".
Even then, when you get the whole thing working, you still have to know Objective-C. To some extent there is no getting around that without someone writing a giant abstraction for you: Apple's frameworks are all written in Objective-C, and their APIs are documented in it. However, that doesn't mean you really need to also learn all the quirks of C syntax or be forced to use Objective-C libraries to solve all of your problems when you are already deep into something else.

This same issue came up on the desktop, and spawned a number of projects that connect Objective-C with other languages. This is an area I have recently gotten into, writing JocStrap, which unifies Java and Objective-C's object models, allowing one to, for instance, develop for the iPhone in Java. Having done this then put me in the perfect frame of mind to try to get other languages over, and the #1 request was Python, which already has an amazing project behind it: PyObjC.
The effort for porting this turned out to be minimal, and the maintainer of the project (Ronald Oussoren) has contacted me about merging my changes, which means that its support that is likely to stay around for quite a while. To get it, one need only install Cydia Packager (which contains a bootstrap of the Telesphoreo project), and install the PyObjC package under the Python section (or apt-get install pyobjc).

A Sample Application

In addition to this, however, I have included a sample program called HelloPython that performs the same function (with almost identical code) to my existing HelloJava and HelloScript (JavaScript) programs: use SQLite3 to access your contact information, and display a nice iPhone UITable with the results. Great thanks goes out to Dave Arter for this, who did much of the implementation of this sample application (I being quite busy and not even knowing Python, which luckily was not needed to port PyObjC).
To get this program, and its source code, install the package iPhone/Python from the Python section (or apt-get install iphone-python). This will install all required dependencies (including PyObjC) and place HelloPython.app in /Applications, inside which is the source code (which is an "of course" given that it's Python). The complete source code for this may also be seen online from my Subversion repository.

A Quick Introduction

In order to help users who are new to PyObjC be able to use this bridge effectively, I think it useful to quickly go through a few points of this file. I am not going to attempt to replace the PyObjC manual, but do think that people who are already familiar with Apple's SDK (and who already know Python) should be able to jump into this almost immediately given just a few pointers.
import objc from _uicaboodle import UIApplicationMain from objc import YES, NO, NULL from sqlite3 import dbapi2 as sqlite
To start with, we see the block of import statements. The last one is from Python's included SQLite3 support, but the first three are new. In Objective-C, boolean values are specified using single-byte 0/1 values represented by the constants YES/NO. In order to make certain to use the correct ABI, PyObjC therefore provides YES/NO constants to use in these cases. (Note that I'm actually not certain that it does this for ABI purposes, but if it doesn't then it should.)
The weirdest part, though, is NULL. This is there because Python doesn't really have a null value: it's None is just another object that an object may choose to refer to. Isn't that enough? If you pass None to an Objective-C message, PyObjC will pass NULL. Unfortunately, there is another case: messages that take a pointer to an object. These come up a lot with error handling (where the message will fill in the pointer to point to a description of the problem).
This form of API is mapped to Python byref argument passing by PyObjC... which makes passing None not helpful if you really want to pass a NULL pointer (to disable error handling, which might be useful). Therefore, we get another constant NULL that disables the byref behavior.
Finally there is the line importing UIApplicationMain from _uicaboodle. This is probably temporary, and might get better addressed by the author of PyObjC as he merges in this support. UIApplicationMain, the first function called by an Objective-C iPhone Application, is itself exported as a C API, which means it can't be automatically discovered by PyObjC.
Functions like this would normally be supported, I believe, by a system known as BridgeSupport, but I didn't have enough time to figure out how. (Besides, the equivalent desktop function NSApplicationMain doesn't use BridgeSupport anyway, so maybe it isn't even encouraged.) We can see it used at the very bottom of the file.
UIApplicationMain(["HelloPython"], PYApplication)
This function takes first the set of arguments passed to the program, with the first argument being the name of the project. I've obviously cheated here, so if anyone can tell me how to get the arguments that were passed to the program (and maybe even verify that doing so works out alright), I will happily modify the program. The second argument s a class that derives from UIApplication that will be instantiated and provide the actual functionality of the program.
To get at classes such as UIApplication, we have to have PyObjC parse the Objective-C frameworks. This is done using objc.loadBundle, which takes both the name of the framework and its path. Here I bring in both Celestial (which has the AV* classes for multimedia support) and UIKit.
objc.loadBundle("Celestial", globals(), "/System/Library/Frameworks/Celestial.framework") objc.loadBundle("UIKit", globals(), "/System/Library/Frameworks/UIKit.framework")
After this, you generally can just directly use an Objective-C class by name as if it were a Python class object, calling static methods on it to send messages to its metaclass. As a simple example we see getting the default size of a navigation bar.
navsize = UINavigationBar.defaultSize() navrect = ((0, 0), (inner[1][0], navsize[1]))
This function returns a struct of two elements (a width/height), which we can see on the next line is accessed using array syntax. PyObjC does not attempt to provide access to the names of the elements as this information is usually not provided in the binary frameworks. If you have nested structs (such as a rectangle, which consists of a point and a size) then they are represented as nested arrays.
While this is all well and good for no-argument or even one-argument messages, in Objective-C multiple arguments are provided using an infix keyword notation that Python does not itself support. PyObjC therefore has to translate this functionality, and chooses to convert all the :'s in a raw selector to _'s. Something that used to be written [a does:b to:c] thereby gets converted to a.does_to_(b, c).
col = UITableColumn.alloc().initWithTitle_identifier_width_("Name", "name", 320)
This example also demonstrates another difference: how objects are instantiated. In Objective-C, to allocate an object, you normally send the message alloc to the metaclass object you wish to obtain an instance of. This, however, isn't the only way to get a new object as you could also call allocWithZone, and technically it is only a convention as a message named connectToServer could also serve this function. PyObjC directly maps this mechanism to Python.
The last thing we have to look at is how to provide messages back to Objective-C. This is done quite easily by providing a method with the appropriately mangled name, but there's a catch: in Objective-C the receiver is expected to know how to parse the arguments that it is passed, whereas the sender is allowed to be ignorant. This functionality comes up while connecting to remote proxy objects, or when attempting to reflect a message to determine its arguments.
This causes Python a greater challenge than some other languages as it doesn't use declared argument types. While it is important to Objective-C whether a message takes a double or a pointer, to Python there is no difference. When overriding an existing message PyObjC can just follow the inheritence hierarchy to figure it out, but barring that case the signature needs to be specified somewhere.
In PyObjC, you specify this with a special annotation objc.signature, which takes the Objective-C type signature of the method. This signature is made up of a number of different characters that represent the type. A complete list of these signature elements can be found in Apple's developer documentation.
@objc.signature("i@:@i") def sectionList_rowForSection_(self, list, section):
To exemplify this, this method takes two non-self arguments: an object (list) and an integer (section). It also returns an integer. Now, much in the same way that Python represents the self argument explicitely, in Objective-C the type signature also represents a couple hidden arguments. One of them is self, and the other is the name of the message that is being called in the form of a "selector" (which is really just an intern'd string pointer). These arguments, in that order, come before the other arguments that are defined.
Putting all of that together, we get the signature integer, object, selector, object, integer, or i@:@i. This may seem irritating, and it is: normally this sort of thing might be handled via BridgeSupport, with a list of the standard callbacks that an object might receive and their standard signatures. If someone knows more about BridgeSupport and would like to help with this, their contribution would be quite useful (although now that the maintainer of PyObjC is involved, that might be handled pretty quickly, and all we need is a good list).

Any Questions?

After all of that, I hope everything just works for you, but if you'd like to find other people working on iPhone applications in Python, I encourage you to join the iPhone/Python mailing list, hosted at the Telesphoreo project.
Good luck!

No comments:

Post a Comment