6

First off, apologies for the confusing title.

What I am trying to achieve is the following: Suppose I have some function foo which takes a function and an integer as input. e.g.

int foo(int(*func)(), int i) {
    int n = func() + i;
    return n;
}

Now, I'd like to wrap this function in a python extension module. So I start writing my interface:

#include <Python.h>

extern "C" {
    static PyObject* foo(PyObject* self, PyObject* args);
}

static PyMethodDef myMethods[] = {
    {"foo", foo, METH_VARARGS, "Runs foo"},
    {NULL, NULL, 0, NULL}
}

// Define the module
static struct PyModuleDef myModule = {
    PyModuleDef_HEAD_INIT,
    "myModule",
    "A Module",
    -1,
    myMethods
};

// Initialize the module
PyMODINIT_FUNC PyInit_BSPy(void) {
    return PyModule_Create(&myModule);
}

//Include the function
static PyObject* foo(PyObject* self, PyObject* args){
    // Declare variable/function pointer
    int(*bar)(void);
    unsigned int n;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, ..., &bar, &n)) {
        return NULL;
    }
}

Now, when it comes time to parse the input tuple, I get confused, as I'm not really sure how to parse it. The idea is rather simple: I need to be able to call foo(bar(), n) in python. But I could use some help on how to realize this.

Mitchell Faas
  • 430
  • 6
  • 19
  • 1
    I'm not sure this is reasonably possible. Python doesn't have the same concept of pointers that C does. Can you wrap your Python call in a C function and then pass that C function as a parameter to your `foo` function? – Robert Harvey Nov 08 '18 at 20:41
  • Q: Is func() a pointer to a C function, or to a Python function? – paulsm4 Nov 08 '18 at 20:42
  • @RobertHarvey The reason I want to do this is to make a python wrapper for BSPlib (which is a parallel processing backbone). The way you initialize this program in c is by writing a function `void spdm()`, and then calling the function `begin(spdm, n)` with `n` the number of processors. I don't fully understand your question, but it seems like that would defeat the purpose. This should also answer Paulsm4's question: It's a Python function. – Mitchell Faas Nov 08 '18 at 20:46
  • Do you mean http://bsplib.eu/? – melpomene Nov 08 '18 at 20:56
  • 1
    Is the signature of the C function that you are trying to wrap _exactly_ `int foo(int(*func)(void), int i)`? The thing you're trying to do will be much easier if it is really something like `int foo(int (*func)(void *data), int i, void *data)` where `func` will be called with whatever you supply as `data` as its argument. – zwol Nov 08 '18 at 21:13
  • https://stackoverflow.com/questions/52079835/embedding-cpython-how-do-you-constuct-python-callables-to-wrap-c-callback-point is the other way around from this question (C calls Python calls C, where you have Python calls C calls Python) but might still be helpful to you. – zwol Nov 08 '18 at 21:14
  • @melpomene Yes, that's the one. The specific function is found at http://bsplib.eu/logic/init/ – Mitchell Faas Nov 08 '18 at 21:18
  • @zwol The above comment shows a link to the exact function. However, the funtion func won't ever have any arguments. – Mitchell Faas Nov 08 '18 at 21:19
  • This should be reasonably simple with `BSPLib::Classic::Init`. With `bsp_init` it's impossible. – melpomene Nov 08 '18 at 21:22
  • @melpomene I'm not married to either, but my knowledge of how to do this is limited to non-existent. Can you provide me with some resources I could use to figure out how to do this? – Mitchell Faas Nov 08 '18 at 21:26
  • 1
    Aha, an alternative that takes C++ `std::function`! That makes it doable. I'll write an answer. – zwol Nov 08 '18 at 21:40
  • 1
    Since apparently you are targeting CPython, are you not concerned about its [Global Interpreter Lock](https://wiki.python.org/moin/GlobalInterpreterLock), which limits the parallelism achievable within a single Python instance? – John Bollinger Nov 08 '18 at 21:41
  • You can do it with `ctypes` (or `cffi`) callback functions. [Here's a link that demonstrates it](https://stackoverflow.com/questions/17980167/writing-python-ctypes-for-function-pointer-callback-function-in-c) (although I'm sure there are better examples with a more thorough search) – DavidW Nov 08 '18 at 21:48
  • @JohnBollinger To be perfectly honest, I didn't know this was a thing. That said, this is currently primarily a research project. Even if GIL ends up being a limiting factor, I'll still be able to gain some insights, and perhaps even find a way around it. This is still uncharted waters. – Mitchell Faas Nov 08 '18 at 21:57
  • 2
    Given that this is a parallel processing use case, you may need to deal with [the interface for calling Python functions from threads not created by Python](https://docs.python.org/3/c-api/init.html#non-python-created-threads). – user2357112 Nov 08 '18 at 22:09

2 Answers2

6

First off, when you have a Python "extension method", implemented in C, and that function receives a Python callable as an argument, here is how you receive the argument, and how you call the callable:

/* this code uses only C features */
static PyObject *
foo(PyObject *self, PyObject *args)
{
    PyObject *cb;    

    // Receive a single argument which can be any Python object
    // note: the object's reference count is NOT increased (but it's pinned by
    // the argument tuple).
    if (!PyArg_ParseTuple(args, "O", &cb)) {
        return 0;
    }
    // determine whether the object is in fact callable
    if (!PyCallable_Check(cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }
    // call it (no arguments supplied)
    // there are a whole bunch of other PyObject_Call* functions for when you want
    // to supply arguments
    PyObject *rv = PyObject_CallObject(cb, 0);
    // if calling it returned 0, must return 0 to propagate the exception
    if (!rv) return 0;
    // otherwise, discard the object returned and return None
    Py_CLEAR(rv);
    Py_RETURN_NONE;
}

The problem with using logic like this to wrap bsp_init is that the pointer to the Python callable is a data pointer. If you passed that pointer directly to bsp_init, bsp_init would attempt to invoke data as machine code and it would crash. If bsp_init passed through a data pointer to the function that it calls, you could work around this with a "glue" procedure:

/* this code also uses only C features */
struct bsp_init_glue_args {
   PyObject *cb;
   PyObject *rv;
};
static void
bsp_init_glue(void *data)
{
   struct bsp_init_glue_args *args = data;
   args->rv = PyObject_CallObject(args->cb, 0);
}

static PyObject *
foo(PyObject *self, PyObject *args)
{
    bsp_init_glue_args ba;
    if (!PyArg_ParseTuple(args, "O", &ba.cb)) {
        return 0;
    }
    if (!PyCallable_Check(ba.cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }
    bsp_init(bsp_init_glue, (void *)&ba, ...);
    if (ba->rv == 0) return 0;
    Py_CLEAR(ba->rv);
    Py_RETURN_NONE;
}

Unfortunately, bsp_init does not have this signature, so you cannot do this. But the alternative interface BSPLib::Classic::Init takes a std::function<void()>, which is an object-oriented wrapper around the pattern above, so you can do this instead:

/* this code requires C++11 */
static PyObject *
foo(PyObject *self, PyObject *args)
{
    PyObject *cb;
    PyObject *rv = 0;
    if (!PyArg_ParseTuple(args, "O", &cb)) {
        return 0;
    }
    if (!PyCallable_Check(cb)) {
        PyErr_SetString(PyExc_TypeError, "foo: a callable is required");
        return 0;
    }

    std::function<void()> closure = [&]() {
       rv = PyObject_CallObject(cb, 0);
    };
    BSPLib::Classic::Init(closure, ...);

    if (rv == 0) return 0;
    Py_CLEAR(rv);
    Py_RETURN_NONE;
}

The magic here is all in the [&]() { ... } notation, which is syntactic sugar for defining and creating an instance of a local class that "captures" the variables cb and rv so that the code inside the curly braces, which will be compiled as a separate function, can communicate with foo proper. This is a C++11 feature called "lambdas", which is a jargon term going all the way back to the earliest days of theoretical CS and immortalized by Lisp. Here is a tutorial, but I am not sure how good it is because I already know the concept inside and out.

It is not possible to do this in plain C, but it isn't possible to call BSPLib::Classic::Init from plain C either (because you can't define a std::function object at all in plain C ... well, not without reverse engineering the C++ standard library and ABI, anyway) so that's okay.

melpomene
  • 84,125
  • 8
  • 85
  • 148
zwol
  • 135,547
  • 38
  • 252
  • 361
  • Thanks for this answer! It took me a while to comprehend what was going on, but seems really cool. I'm not able to verify whether it works yet, since I ran in to a bug, but will update and accept once I can! – Mitchell Faas Nov 08 '18 at 22:59
  • Turns out this is totally an answer to the question I was asking! Sadly, as @user2357112 noted in the main comment thread, it seems like this method results in multiple threads trying to execute Python, which is causing problems. I may have a way around this, but I'm not sure. Either way, this answer was extremely helpful! – Mitchell Faas Nov 29 '18 at 01:04
  • @MitchellFaas Depending what you're trying to do, Python's "multiple interpreters" mechanism may be helpful. I don't know anything about BSPLib though, so I can't be sure. – zwol Nov 29 '18 at 03:34
1

The Python callable passed in will be a Python object which matches the Callable protocol. Therefore, in order to pass it to your C function, you'll have to create another C function as a proxy which matches the required signature of your function pointer.

As an example:

static PyObject* foo_cb_callable;
static int py_foo_callback(void) {
    PyObject* retval;
    int result;

    // Call the python function/object saved below
    retval = PyObject_CallObject(foo_cb_callable, NULL);

    // Convert the returned object to an int if possible
    if (retval && PyInt_Check(retval))
        result = (int)PyInt_AsLong(retval);
    else
        result = -1;
    Py_XDECREF(retval);
    return result;
}

// NOTE: I renamed this to avoid conflicting with your "foo"
// function to be called externally.
static PyObject* py_foo(PyObject* self, PyObject* args) {
    unsigned int n;
    int result;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, "OI", &foo_cb_callable, &n)) {
        return NULL;
    }
    // Ensure the first parameter is a callable (e.g. function)
    if (!PyCallable_Check(foo_cb_callable)) {
        return NULL;
    }

    // Call foo with our custom wrapper
    result = foo(&py_foo_callback, n);
    return Py_BuildValue("i", result);
}

Note that I used a global callback object pointer in the example, since your original function pointer didn't have a place for custom user data. If you add a generic user parameter to the func callback, the wrap_foo_callback could be passed to it that way instead of making a global variable. For example:

int foo(int(*func)(void*), void* user_data, int i) {
    int n = func(user_data) + i;
    return n;
}

// ...

static int py_foo_callback(void* callback) {
    PyObject* retval;
    int result;

    // Call the python function/object saved below
    retval = PyObject_CallObject((PyObject*)callback, NULL);

    // Convert the returned object to an int if possible
    if (retval && PyInt_Check(retval))
        result = (int)PyInt_AsLong(retval);
    else
        result = -1;
    Py_XDECREF(retval);
    return result;
}

static PyObject* py_foo(PyObject* self, PyObject* args) {
    PyObject* callback;
    unsigned int n;
    int result;

    // Parse the input tuple
    if (!PyArg_ParseTuple(args, "OI", &callback, &n)) {
        return NULL;
    }
    // Ensure the first parameter is a callable (e.g. function)
    if (!PyCallable_Check(callback)) {
        return NULL;
    }

    // Call foo with our custom wrapper
    result = foo(&py_foo_callback, callback, n);
    return Py_BuildValue("i", result);
}
Zrax
  • 1,641
  • 10
  • 15