2

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.

hoefling
  • 59,418
  • 12
  • 147
  • 194

2 Answers2

3

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Awesome, thank you for the fast answer! Doing my first steps into C and Python C API, so bear with the question stupidity. – hoefling May 24 '20 at 18:54
0

This is the end result, in case someone needs a working snippet:

static PyObject* MyLib_PyValue(PyObject *self, PyObject *args, PyObject *kwargs) {
    static char *kwlist[] = { "x", NULL };
    PyObject *pyX = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|O:value", kwlist, &pyX)) {
        return NULL;
    }
    if (pyX == NULL) { // getter
        return PyLong_FromLong(APIGetValue());
    }
    // setter
    int x = (int)PyLong_AsLong(pyX);
    if (PyErr_Occurred()) {
        return NULL;
    }
    if (x < 0) {
        PyErr_SetString(PyExc_ValueError, "x must be positive");
        return NULL;
    }
    APISetValue(x);
    Py_RETURN_NONE;
}
hoefling
  • 59,418
  • 12
  • 147
  • 194