Wednesday, November 25, 2009

Bridging from Objective-C into Python

Here is my code following bbum's implementation of a worked out example of how to call Python from Objective-C. This follows up on a question I asked on Stack Overflow.

An important part of the approach is that although we want to do this:

ObjC => Py

and we define an Abstract class in Objective-C from which the Python class inherits, so we have this:

ObjC => Py Concrete (ObjC Abstract)

we actually start from Python. This gives us the running interpreter:

Py => ObjC => Py Concrete

But the essential aspect of this method, that was really new to me, is a pair of functions that take the name of a class (as a string), and return the class object itself. In Python (using objc):

objc.lookUpClass("Abstract")

And in Objective-C:

Class c = NSClassFromString(@"Concrete");

I probably should have known about the first one. After all, it is mentioned very early in the Userguide for PyObjC, but after some searching, it is not clear to me where the latter is defined, let alone documented.

I have stripped bbum's example down a bit, and also, just pass in a dictionary to the class we will instantiate in Python.


Part One


an Objective-C class---Abstract. It contains a class method that will allocate and return an instance. This method takes a dictionary that can hold any arguments we want to pass in.


#import <Cocoa/Cocoa.h>

@interface Abstract : NSObject {
NSMutableDictionary *mD;
}
@property (retain) NSMutableDictionary *mD;
+ myInstance: (NSMutableDictionary *)aD;
- (NSMutableDictionary *)report;
@end



#import "Abstract.h"

@implementation Abstract
@synthesize mD;

+ myInstance: (NSMutableDictionary *)aD {
return nil;
}
- (NSMutableDictionary *)report {
return [self mD];
}
@end


I put this code into files Abstract.h and Abstract.m in a new Xcode project that builds a bundle (what the docs call a "loadable bundle"). Make sure you select the Cocoa framework to link against. Select Build from the Build menu (the Toolbar Item is not enabled). I copied Abstract.bundle to the top-level directory that contains the Python script which will use it.


Part Two


a Python class---Concrete.py. The first section loads the Abstract bundle. This somehow makes the class definition "available" to the Objective-C runtime, so we can use a the handy method lookUpClass to actually load it. This allows us to subclass Abstract in Python.


import objc
from Foundation import NSBundle

p = '~/Desktop/ObjCPy/Abstract.bundle'
bundle = NSBundle.bundleWithPath_(p)
if not bundle.principalClass():
print "%s: failed to load bundle." % p

Abstract = objc.lookUpClass("Abstract")

class Concrete(Abstract):

@classmethod
def myInstance_(self, D):
print "creating Python instance"
c = Concrete.new()
self.mD = D
self.mD['lastName'] = 'MacGillicuddy'
return c

def report(self):
return self.mD



Part Three


another Objective-C class---Caller. This is in a separate Xcode project that is built exactly as above. The code couldn't be simpler.


#import <Cocoa/Cocoa.h>
@interface Caller : NSObject {
}
+ (NSString *) callPython;
@end



#import "Caller.h"
#import "Abstract.h"

@implementation Caller
+ (NSString *) callPython;
{
NSMutableDictionary *temp;
temp = [NSMutableDictionary dictionaryWithCapacity:5];
[temp setObject:@"Meredith" forKey:@"firstName"];

Class c = NSClassFromString(@"Concrete");
AbstractClass *obj = [c myInstance:temp];
NSLog(@"Created instance: %@", obj);
NSLog(@"OC dict: %@", temp);
NSLog(@"obj mD %@", [[obj mD] description]);
NSLog(@"report %@", [[obj report] description]);
return [NSString stringWithFormat: @"Did it!"];
}
@end


The class contains a single class method that calls the Python class method myInstance. We set one key-value pair in a dictionary on the Objective-C side, then pass it into Python. The handy method NSClassFromString makes the Python class available. We try two approaches to access the dictionary: via the getter method that is set up for us via @property and @synthesize (see Abstract), and direct access via report instead, breaking encapsulation.


Part Four


a Python script that sets everything in motion.


import sys
import objc
from Foundation import NSBundle
import Concrete

p = '~/Desktop/ObjCPy/Caller.bundle'
bundle = NSBundle.bundleWithPath_(p)
if not bundle.principalClass():
print "%s: failed to load bundle." % p

caller = objc.lookUpClass("Caller")
print caller
result = caller.callPython()
print result


And the output:


$ python script.py 
<objective-c class Caller at 0x1030fb150>
creating Python instance
Python[302:d07] Created instance: <Concrete: 0x10361fdc0>
Python[302:d07] OC dict: {
firstName = Meredith;
lastName = MacGillicuddy;
}
Python[302:d07] obj mD (null)
Python[302:d07] report {
firstName = Meredith;
lastName = MacGillicuddy;
}
Did it!


The Caller class (ObjC) adds one key-value pair to the dictionary, while the Concrete class (Python) adds the second, and the dictionary is still available from the returned Python instance. One thing: the getter implemented via @property does not seem to work from Python, but an explicit report method does.

The Python Concrete class could then call any further Python code you want. Neat! Thanks, Bill.

No comments: