Search code examples
pythonc++callbackswig

Calling undefined number of callback functions from the C++ layer


Recently I started to use SWIG to develop a solution that passes (possibly) several callback functions that should be called in the C++ layer. Besides, I spent some time reading the introduction callback chapter and the director feature.

I've managed to achieve my goal by coding different classes that inherit from the base class in the C++ layer from the documentation example, but I'm trying to learn in how to pass std::function instances instead of pointer to classes.

I believe that the (incorrect) example below summarizes my objective:

File example.h:

#include <functional>
#include <map>
#include <vector>

class Controller {
 private:
  std::vector<std::function<void()>> functions;

 public:
  virtual void AddFunction(const std::function<void(void)>& func) {
    functions.push_back(func);
  }

  virtual void Execute() {
    for (int m = 0; m < functions.size(); ++m) functions[m]();
  }

  Controller() {}
  virtual ~Controller() {}
};

File example.i:

%module example
%{
#include "example.h"
%}

%include "example.h"

File example.py:

import example

def foo():
    print("yey")

def bar():
    print("dog")

def cat():
   print("miaw")

e = example.Controller()
e.AddFunction(foo)
e.AddFunction(bar)
e.AddFunction(cat)

# Prints yey, dog and miaw sequentially
e.Execute()

So, my question is:

  1. Am I in the right direction here? In other words, is this really possible to do with SWIG?
  2. Can I achieve my goal with the director feature only?

Solution

  • To use directors a class with a virtual function to override is needed.

    Given your Controller definition as test.h:

    #include <functional>
    #include <map>
    #include <vector>
    
    class Controller {
     private:
      std::vector<std::function<void()>> functions;
    
     public:
      virtual void AddFunction(const std::function<void(void)>& func) {
        functions.push_back(func);
      }
    
      virtual void Execute() {
        for (int m = 0; m < functions.size(); ++m) functions[m]();
      }
    
      Controller() {}
      virtual ~Controller() {}
    };
    

    Then the SWIG interface file (test.i) could declare a director-enabled Callback class and extend Controller to take that class as a parameter and call the overridden virtual function:

    %module(directors="1") test
    
    %feature("director") Callback;
    
    %inline %{
    class Callback {
    public:
        virtual void function() = 0;
        virtual ~Callback() {}
    };
    %}
    
    %{
    #include "test.h"
    %}
    %include "test.h"
    
    %extend Controller {
        void AddFunction(Callback* cb) {
            $self->AddFunction(std::bind(&Callback::function, cb));
        }
    }
    

    The Python code would look like:

    import test
    
    class Foo(test.Callback):
        def function(self):
            print('yey')
    
    class Bar(test.Callback):
        def function(self):
            print('dog')
    
    class Cat(test.Callback):
        def function(self):
            print('miaw')
    
    # Note that AddFunction needs to be provided an object instance that persists
    # until after Execute is called, so "e.AddFunction(Foo())" wouldn't work
    # because Foo() would be freed after the AddFunction call.
    foo = Foo()
    bar = Bar()
    cat = Cat()
    
    e = test.Controller()
    e.AddFunction(foo)
    e.AddFunction(bar)
    e.AddFunction(cat)
    e.Execute()
    

    Output:

    yey
    dog
    miaw
    

    This is the .i file to use to directly add functions and not use directors. This works with the example.py in the question and is based on this answer:

    %module example
    
    %inline %{
    class PyCallback
    {
        PyObject *func;
        PyCallback& operator=(const PyCallback&); // Not allowed
    public:
        PyCallback(const PyCallback& o) : func(o.func) {
            Py_XINCREF(func);
        }
        PyCallback(PyObject *func) : func(func) {
            Py_XINCREF(this->func);
            assert(PyCallable_Check(this->func));
        }
        ~PyCallback() {
            Py_XDECREF(func);
        }
        void operator()(void) {
          if (!func || Py_None == func || !PyCallable_Check(func))
              return;
          PyObject *result = PyObject_CallNoArgs(func);
          Py_XDECREF(result);
        }
    };
    %}
    
    %{
    #include "test.h"
    %}
    %include "test.h"
    
    %extend Controller {
        void AddFunction(PyObject* cb) {
            $self->AddFunction(PyCallback(cb));
        }
    }