Search code examples
pythoncpython-c-api

Python C extension function that accepts optional integer


I want to implement a following Python function in a C extension module:

def value(x: Optional[int] = None) -> Optional[int]:
    if x is None:
        # act like a getter
        return APIGetValue()  # retrieve the value from an external library
    # act like a setter
    APISetValue(x)  # pass the value to an external library
    return None

Here is what I got so far:

static PyObject* MyLib_PyValue(PyObject *self, PyObject *args, PyObject *kwargs) {
    static char *kwlist[] = { "x", NULL };
    int x;
    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|i:value", kwlist, &x)) {
        return NULL;
    }
    if (x == NULL) {
        return PyLong_FromLong(APIGetValue());
    }
    APISetValue(x);
    Py_RETURN_NONE;
}

Calling the function with args works, but when calling value() as a getter, I get 1 instead of NULL. How should I proceed? I'm not very familiar with Python's C API yet.


Solution

  • First, you can't have a NULL int. NULL is a thing for pointers. Due to the way C type conversion works and how the NULL macro is defined, x == NULL with an int x usually does one of two things: it behaves as x == 0, or it produces a compile-time error.

    Second, quoting the Python C API argument parsing docs,

    The C variables corresponding to optional arguments should be initialized to their default value — when an optional argument is not specified, PyArg_ParseTuple() does not touch the contents of the corresponding C variable(s).

    This is also true of PyArg_ParseTupleAndKeywords. Your x is uninitialized, and PyArg_ParseTupleAndKeywords doesn't write a value for it, so accessing the value of x in the x == NULL comparison is undefined behavior.

    You need to initialize x, and you need to use a type that actually allows you to detect missing values. That probably means declaring x as PyObject *x = NULL; and passing "|O:value" to PyArg_ParseTupleAndKeywords, then handling the conversion to C long inside your function instead of relying on PyArg_ParseTupleAndKeywords to do it.