1

I'm embedding Python in an Objective C application using PyObjC, setting the Python environment up by hand on the ObjC side (i.e. not using py2app). I'm starting the Python in a separate thread (on the ObjC side) with a call to PyRun_File(), and then one to PyObject_CallFunction() to start it doing something (specifically update some menus, register and handle menu callbacks). The Python function starts the run loop for the secondary thread and the callback class instance hangs around. All of this works. I can pass basic data such as strings to the initial Python function without problem and menu actions work as I'd like.

I'd like to provide a pre-instantiated delegate instance to the Python function for ease of configuration (from the point of view of the Objective C developer). How do I pass an Objective C instance (my delegate) to the Python function? Is it possible? Are there bridge functions I'm missing? Do I need to create a suitably configured PyObject by hand? What sort of conversion should I do on the Python side to ensure the delegate.methods() are usable from Python and proxying works as it should? Are there any memory-management issues I should be aware of (on either side of the bridge)?

I'm using Python 2.7, PyObjC 2.5 and targeting OSX 10.6. Happy to consider changing any of those if the solution specifically demands it. TIA.

2 Answers 2

4

The easiest way to ensure problem free usage of the bridge is to ensure that the delegate methods that you use don't use C arrays as arguments or return values and don't use variadic signatures ("..."). Futhermore ensure that all pass-by-reference arguments (such as the commonly used "NSError**" argument) are marked up with "in", "out" or "inout" to indicate in which direction values are passed. This ensures that the bridge can get all information it needs from the Objective-C runtime.

There are two options to pass a preconstructed object to Python code:

  1. Create a class method that returns the object

  2. Use the PyObjC API to create the python proxy for the Objective-C object.

The latter uses an internal PyObjC API (also used by the framework wrappers) and could break in future versions of PyObjC. That said, I don't have active plans to break the solution I describe here.

First ensure that the right version of "pyobjc-api.h" and "pyobjc-compat.h" are available for the Objective-C compiler.

Use #include "pyobjc-api.h" to make the API available.

Call "PyObjC_ImportAPI" after initialising the Python interpreter, but before you use any other PyObjC function.

Use "pyValue = PyObjC_IdToPython(objcValue)" to create a Python representation for an Objective-C object.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks Ronald. That answers the "Can I do it all on the ObjC side" question, as well as addressing memory issues. It's also fewer lines. I hadn't thought of Option 1, but I think the coupling's a little too tight for what I want to do. Useful to know, though. I'll give #2 it a spin. Would you care to comment on any weaknesses in my answer, above?
BTW. I’m very much interested in the features that would be useful for using PyObjC to embed Python functionality in an Objective-C application. That usecase is not served very well at the moment because I don’t do this myself.
Accepting the answer as the safe approach advocated by the PyObjC developer.
My usecase is a safe and simple scripting/plugin environment (for a CAD application, but ideally a more generic solution). Safe means it doesn't bring down the app unless they try really hard, so separate threads; simple means the developer can include the core in a few lines, provide delegation and namespaced additional API functionality, and the user can write Python as they would at the console. I think most of what I need is there already in PyObjC but instance-passing was a head-scratcher. Packaging likewise, I suspect.
Just to confirm that the above approach works. Pay attention to adding the correct version of the headers to the include path in project settings. After PyObject *mainModule = PyImport_AddModule("__main__"); I did PyObject *success = (PyObject *)PyObjC_ImportAPI(mainModule); and then PyObject *pyDelegate = (PyObject *)PyObjC_IdToPython((id)[self delegate]);. Note the (id) cast. This object can then be passed to PyObject_CallFunctionObjArgs() and used as a native Python instance. Thanks again for the pointers.
1

Well, I found an answer, although it strikes me as a little brittle. Caveats apply to the packaging of the application (ensuring you have the right Python accessible on all platforms) and the use of ctypes.

This answer helped, as did the PyObjC and Python API documentation.

On the ObjC side, something like:

#import "Delegate.h"
Delegate *delegate = [[Delegate alloc] init];

// ... Appropriate PyObjC initialisation, then
PyObject *mainModule = PyImport_AddModule("__main__");
PyObject *globals = PyModule_GetDict(mainModule);
PyRun_File(mainFile, (char *)[[mainFilePath lastPathComponent] UTF8String], Py_file_input, globals, globals);

// Get a reference to the delegate instance
uintptr_t delegate_pointer = (uintptr_t)delegate;

// Get something callable in the Python file...
PyObject *pFunc = PyObject_GetAttrString(mainModule, "myCallable");
if (pFunc && PyCallable_Check(pFunc)) {
    // ... and call it with an appropriately boxed reference to our delegate
    PyObject_CallFunctionObjArgs(pFunc, PyInt_FromLong(delegate_pointer), nil);
}

//   It's also possible to instantiate a Python class and call a method on it directly from ObjC:
//
//   PyObject *klass = PyObject_GetAttrString(mainModule, "PythonClass");
//   # Here I assume an NSObject subclass, hence alloc().init().  Pure Python instance instantiation will of course differ
//   PyObject *instance = PyObject_CallMethod(PyObject_CallMethod(klass, "alloc", nil), "init", nil);
//   PyObject_CallMethodObjArgs(mgr, PyString_FromString([@"myMethod" cStringUsingEncoding:NSUTF8StringEncoding]), PyInt_FromLong(delegate_pointer), nil);

On the Python side of things, in myMethod() (or myCallable()):

def myCallable(delegateID):
    _objc = ctypes.PyDLL(objc._objc.__file__)
    _objc.PyObjCObject_New.restype = ctypes.py_object
    _objc.PyObjCObject_New.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int]
    delegate = _objc.PyObjCObject_New(delegateID, 0, 1)

    # Call a method on the ObjectiveC delegate, let off fireworks, etc.
    delegate.aMethod()

Hope that helps someone else.

I still have outstanding questions about whether there are better alternatives, whether the bridge API has a way of doing this all on the ObjC side, and memory; I'll ask separate questions if needs-be.

2 Comments

This probably works as well, but is poking directly into PyObjC internals and because of that is fairly unsafe. I’m not sure that it even works at all with the way I usually build PyObjC myself (with linker flags that hide almost all symbols except the few that need to be available for Python to load the module)
Sorry, may I know how to pass args(ex. int, string) to delegate method and how to define type in C/Objc?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.