Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python-C Api wrapper in Objective-C crashes with call to __getattr__ when passed a Python Object

I'm in the process of writing a lightweight interface in Objective-C that is capable of executing python scripts and passing data back and forth between Objective-C and Python. I've looked into PyObjC and ObjP and neither are what I'm looking for (and since I'm developing for iOS <= 6.0.1 PyObjC won't compile do to the heavy use of NSMapTable's).

So basically I created a Python Type in Objective-C called "ObjC_Class" (creative, no?) and I want this Python object to be nearly analogous to an ObjC object. So I decided to override the __getattr__ function of the class so I can access arbitrary methods and properties of the ObjC equivalent of that class.

Here is the code:

static PyObject * ObjC_Class_getattro(ObjC_Class *self, PyObject *name)
{
    NSString *attrName = [NSString stringWithCString:PyString_AsString(name) encoding:NSUTF8StringEncoding];

    NSLog(@"Calling Object: %@", self->object);
    if([self->object respondsToSelector:NSSelectorFromString(attrName)])
    {
        methodName = attrName;
        //PyObject* (*fpFunc)(PyObject*,PyObject*) = ObjC_Class_msg_send;
        PyMethodDef methd = {[attrName UTF8String],ObjC_Class_msg_send,METH_VARARGS,[attrName UTF8String]};
        PyObject* pyName = PyString_FromString(methd.ml_name);
        PyObject* pyfoo = PyCFunction_NewEx(&methd,(PyObject*)self,pyName);
        Py_DECREF(name);

        return pyfoo;
    }
    else
    {
        return name;
    }
}

Now, it works perfectly fine when I say:

Example #1

from ObjC import ObjC_Class
new = ObjC_Class('UIView')
new.backgroundColor("asdf", 42, "some_random_string") # This does not crash

But when I run:

Example #2

from ObjC import ObjC_Class
new = ObjC_Class('UIView')
moreStuff = "some_random_string" # or "42" or [1,2,3] or anything else...
new.backgroundColor("asdf", 42, moreStuff) # !!! This does crash

It crashes saying:

error: address doesn't contain a section that points to a section in a object file

I've seen this error when I try calling nonexistent functions but I can't imagine why the first would work but the second wouldn't.

Here is the implementation of the ObjC_Class_msg_send function:

static PyObject* ObjC_Class_msg_send(PyObject *self, PyObject *args)
{
    NSLog(@"Entering...");
    NSMutableArray *tmp = [[NSMutableArray alloc] init];
    for(int i=0;i<PyTuple_Size(args);i++)
    {
        [tmp addObject:Py_to_ObjC(PyTuple_GetItem(args, i))];
    }
    NSLog(@"Object: %@, Method Name: %@, args: %@", ((ObjC_Class*)self)->object, methodName, tmp);
    methodName = @"";
    return PyString_FromString("Did it actually work!?!?!");
}

When I run the example that passes a python variable into the function it crashes before ObjC_Class_msg_send is even called (but after ObjC_Class_getattro returns its value).

(Oh and please excuse the sloppy code... I'm working on getting a simple proof-of-concept running before allocating too much time for this project)

Something I failed to mention: My ObjC_Class has an element named 'object' which is of type 'id' which is what stores the reference to the Objective-C object that the Python object is representing...

One more side note: I'm linking against Python 2.6(.3?) which has been mostly statically-linked

UPDATE I've managed to get it to stop crashing by defining fpFunc as static removing fpFunc altogether... I'm not even sure why I had it in there (stupid copy+paste...):

static PyObject * ObjC_Class_getattro(ObjC_Class *self, PyObject *name)
{
    NSString *attrName = [NSString stringWithCString:PyString_AsString(name) encoding:NSUTF8StringEncoding];

    NSLog(@"Calling Object: %@", self->object);
    if([self->object respondsToSelector:NSSelectorFromString(attrName)])
    {
        methodName = attrName;
        //static PyObject* (*fpFunc)(PyObject*,PyObject*) = ObjC_Class_msg_send; // static now
        PyMethodDef methd = {[attrName UTF8String],ObjC_Class_msg_send,METH_VARARGS,[attrName UTF8String]};
        PyObject* pyName = PyString_FromString(methd.ml_name);
        PyObject* pyfoo = PyCFunction_NewEx(&methd,(PyObject*)self,pyName);
        Py_DECREF(name);

        return pyfoo;
    }
    else
    {
        return name;
    }
}

But... Now Python is throwing the error (when I pass a python variable in as an argument, i.e.: Example #2 above):

SystemError: Objects/methodobject.c:120: bad argument to internal function

:( I've never seen this one before...

like image 629
George Mitchell Avatar asked Jan 29 '13 16:01

George Mitchell


1 Answers

Well this is embarrassing... I found the issue (in case anybody else is having the same problem(s)). I traced the error through the python source and noticed that the error was being thrown here:

PyObject *
PyCFunction_Call(PyObject *func, PyObject *arg, PyObject *kw)
{
    PyCFunctionObject* f = (PyCFunctionObject*)func;
    PyCFunction meth = PyCFunction_GET_FUNCTION(func);
    PyObject *self = PyCFunction_GET_SELF(func);
    Py_ssize_t size;

    switch (PyCFunction_GET_FLAGS(func) & ~(METH_CLASS | METH_STATIC | METH_COEXIST)) {
    case METH_VARARGS:
        if (kw == NULL || PyDict_Size(kw) == 0)
            return (*meth)(self, arg);
        break;
    case METH_VARARGS | METH_KEYWORDS:
    case METH_OLDARGS | METH_KEYWORDS:
        return (*(PyCFunctionWithKeywords)meth)(self, arg, kw);
    case METH_NOARGS:
        if (kw == NULL || PyDict_Size(kw) == 0) {
            size = PyTuple_GET_SIZE(arg);
            if (size == 0)
                return (*meth)(self, NULL);
            PyErr_Format(PyExc_TypeError,
                "%.200s() takes no arguments (%zd given)",
                f->m_ml->ml_name, size);
            return NULL;
        }
        break;
    case METH_O:
        if (kw == NULL || PyDict_Size(kw) == 0) {
            size = PyTuple_GET_SIZE(arg);
            if (size == 1)
                return (*meth)(self, PyTuple_GET_ITEM(arg, 0));
            PyErr_Format(PyExc_TypeError,
                "%.200s() takes exactly one argument (%zd given)",
                f->m_ml->ml_name, size);
            return NULL;
        }
        break;
    case METH_OLDARGS:
        /* the really old style */
        if (kw == NULL || PyDict_Size(kw) == 0) {
            size = PyTuple_GET_SIZE(arg);
            if (size == 1)
                arg = PyTuple_GET_ITEM(arg, 0);
            else if (size == 0)
                arg = NULL;
            return (*meth)(self, arg);
        }
        break;
    default:
        PyErr_BadInternalCall();
        return NULL;
    }
    PyErr_Format(PyExc_TypeError, "%.200s() takes no keyword arguments",
             f->m_ml->ml_name);
    return NULL;
}

So I thought, "Maybe my PyMethodDef methd is out of scope (or something) by the time Python takes a look at it" (idk if Python does different processing on functions that contain arguments that are variables than on functions that have const arguments which cause the former to be delayed somewhere up the pipeline...).

So I pulled methd out of the function and declared it as static (this sounds familiar...) and voila! It works beautifully. Here is the updated code:

static PyMethodDef methd = {"blah",ObjC_Class_msg_send,METH_VARARGS,"blech"};

static PyObject * ObjC_Class_getattro(ObjC_Class *self, PyObject *name)
{
    NSString *attrName = [NSString stringWithCString:PyString_AsString(name) encoding:NSUTF8StringEncoding];

    NSLog(@"Calling Object: %@", self->object);
    if([self->object respondsToSelector:NSSelectorFromString(attrName)])
    {
        methodName = attrName;
        PyObject* pyName = PyString_FromString(methd.ml_name);
        PyObject* pyfoo = PyCFunction_NewEx(&methd,(PyObject*)self,pyName);
        Py_DECREF(name);

        return pyfoo;
    }
    else
    {
        return name;
    }
}

Now, if you'll excuse me, I'm going to go study 'static'.

like image 185
George Mitchell Avatar answered Oct 11 '22 16:10

George Mitchell