Search code examples
pythoncpython-3.xpython-c-apicpython

Passing line number to embedded Python interpreter


So I have a C program which uses an embedded CPython interpreter to execute Python code. The problem is that if the Python code has an error, the line number information provided by the interpreter is sort of useless, because each call to PyEval_EvalCodeEx begins counting lines at 1. So, I'd like to give the Python interpreter a context, in terms of line numbers, whenever I execute code.

Is there a way to do this? Looking at the definition of PyEval_EvalCodeEx, which is ultimately the lowest level function for code execution that is exposed by the Python C-API, I don't see any opportunity to pass in line number information.

The docs simply read:

PyObject* PyEval_EvalCodeEx(PyObject *co, PyObject *globals, PyObject
*locals, PyObject **args, int argcount, PyObject **kws, int kwcount, PyObject **defs, int defcount, PyObject *closure)

Evaluate a precompiled code object, given a particular environment for its evaluation. This environment consists of dictionaries of global and local variables, arrays of arguments, keywords and defaults, and a closure tuple of cells.

So is this simply not possible to do?


Solution

  • If you look at the implementation of PyEval_EvalCodeEx(), you may see that the first argument is casted to PyCodeObject *, right at the beginning of the function body:

    /* File: ceval.c */
    
    PyObject *
    PyEval_EvalCodeEx(PyObject *_co, ...)
    {
        PyCodeObject* co = (PyCodeObject*)_co;
        ...
    }
    

    And if you look at the PyCodeObject, there is a member which is called co_firstlineno:

    /* File: code.h */
    
    typedef struct {
        PyObject_HEAD
        ...
        PyObject *co_filename;  /* unicode (where it was loaded from) */
        PyObject *co_name;      /* unicode (name, for reference) */
        int co_firstlineno;     /* first source line number */
        PyObject *co_lnotab;    /* string (encoding addr<->lineno mapping) See
                                   Objects/lnotab_notes.txt for details. */
        ...
    } PyCodeObject;
    

    So I guess it might be sufficient to modify the co_firstfileno field before calling the PyEval_EvalCodeEx() function, like this:

    ((PyCodeObject *)co)->co_firstlineno = 42;
    PyEval_EvalCodeEx(co, ...);
    

    That should be enough, and as far as I remember, you don't even need to modify the co_lnotab, because it contains offsets from co_firstlineno rather than absolute locations.