0

I have a function in c++ that receives a initialised class as a PyObject. The python class is:

class Expression:
    def __init__(self, obj):
        self.obj = obj

    def get_source(self):
        #Check if the object whose source is being obtained is a function.
        if inspect.isfunction(self.obj):
            source = inspect.getsourcelines(self.obj)[0][1:]
            ls = len(source[0]) - len(source[0].lstrip())
            source = [line[ls:] for line in source]
            #get rid of comments from the source
            source = [item for item in source if item.lstrip()[0] != '#']
            source = ''.join(source)
            return source
        else:
            raise Exception("Expression object is not a function.")

The c++ receives this:

Expression(somefunctogetsource)

From c++ how do I call the get_source method of the expression object? So far I've read the python c-api docs and tried things like this:

PyObject* baseClass = (PyObject*)expression->ob_type;
PyObject* func = PyObject_GetAttrString(baseClass, "get_source");
PyObject* result = PyObject_CallFunctionObjArgs(func, expression, NULL);

And convert the result to a string, but this doesn't work.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Methodicle
  • 27
  • 8

1 Answers1

2

Simpler than you're making it. You don't need to retrieve anything from the base class directly. Just do:

PyObject* result = PyObject_CallMethod(expression, "get_source", NULL);
if (result == NULL) {
    // Exception occurred, return your own failure status here
}
// result is a PyObject* (in this case, it should be a PyUnicode_Object)

PyObject_CallMethod takes an object to call a method of, a C-style string for the method name, and a format string + varargs for the arguments. When no arguments are needed, the format string can be NULL.

The resulting PyObject* isn't super useful to C++ code (it has runtime determined 1, 2 or 4 byte characters, depending on the ordinals involved, so straight memory copying from it into std::string or std::wstring won't work), but PyUnicode_AsUTF8AndSize can be used to get a UTF-8 encoded version and length, which can be used to efficiently construct a std::string with equivalent data.

If performance counts, you may want to explicitly make a PyObject* representing "get_source" during module load, e.g. with a global like:

PyObject *get_source_name;

which is initialized in the module's PyMODINIT_FUNC with:

get_source_name = PyUnicode_InternFromString("get_source");

Once you have that, you can use the more efficient PyObject_CallMethodObjArgs with:

PyObject* result = PyObject_CallMethodObjArgs(expression, get_source_name, NULL);

The savings there are largely in avoiding constructing a Python level str from a C char* over and over, and by using PyUnicode_InternFromString to construct the string, you're using the interned string, making the lookup more efficient (since the name of get_source is itself automatically interned when def-ed in the interpreter, no actual memory comparison of the contents takes place; it realizes the two strings are both interned, and just checks if they point to the same memory or not).

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • Ah wow, I really tried making that more difficult for myself. Thanks for explaining it so well. The extra comments about performance are incredibly useful, much appreciated! – Methodicle Mar 22 '19 at 15:19
  • @Methodicle: Glad I could help. Minor side-note: Pretty sure `[item for item in source if item.lstrip()[0] != '#']` is an accident waiting to happen; it'll die with an `IndexError` if one of the source lines is empty (nothing but whitespace). Change `item.lstrip()[0] != '#'` to `not item.lstrip().startswith('#')` to make it safe (`item.lstrip()[:1] != '#'` would also be safe, but less obvious in intent). I'd also recommend changing your `raise Exception` to `raise TypeError`, since the whole point is that the input was of the incorrect type, and more specific exceptions are more helpful. – ShadowRanger Mar 22 '19 at 15:24
  • Wow thanks Again, just before I saw your comment It actually died died because of that exact reason. I must need more practice I thought I'd done well thinking about comment lines but completely forgot empty lines. – Methodicle Mar 22 '19 at 15:31