How do I pass an Objective C instance into a PyObjC Python function when using PyRun_File()/PyObject_CallFunction()?

StackOverflow https://stackoverflow.com/questions/21988970

  •  15-10-2022
  •  | 
  •  

Frage

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.

War es hilfreich?

Lösung

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.

Andere Tipps

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.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top