in these days my work involve the porting of one of mine C++ library to Python, to accomplish this I’ve used Cython. The use of Cython is simple and very well documented but I’ve encountered a problem porting a callback from C++ to Python so I’ve decided to write down a post about my problem and the solution that I’ve adopted.
The library that I’ve wrote is a client library used to communicate with a server, it provide a class with a very poor API, the meaning of this library is out of scope but below you can find the basic actions provided by the library.
init(std::function<void(const unsigned int &,
const std::string &)> log = nullptr,
unsigned int log_level=NORMAL_LVL);
connect(const std::string &ip, unsigned int port);
... // others methods
As said it has a simple API, just one function has need of explanation, the init(). This method initializes the library (or class) itself, it accept a function pointer and an unsigned int. The first parameter is used in order to let the user specify its own custom log method and the second to set the level of the log in which is interested.
Let the user to specify its own log method is important, in this way it can save it on file or handle some casuistry in some way. The constraint is that the function specified must accept as first parameter an unsigned int and for second parameter a string, these are the log level and log message.
The problem is that the porting in Python use Python code (well… kinda obvious) which C++ library don’t understand. In fact even if you use Cython some where you call the C++ code of the original library, in this case it means that when the user pass the callback in Python the C++ part get angry because it expects a function pointer.
To avoid it I’ve created a Men In the Middle function with the same interface expected from a log callback function in C++ which call the Python callback.
# create a MIM function for log callback
# it has the same interface expected from C++ version
cdef void log_mim(unsigned int log_level, const string message)
# Python class
cdef class PyClient:
def init(self, log_cb=None, log_lvl=0):
if log_cb is not None:
# Python 2.x support
if not callable(log_cb):
raise TypeError("""The first parameter MUST
be a function.""")
# store the Python callback on global variable
pylog_cb = log_cb
return self.client_ptr.init(log_mim, log_lvl)
The init method accept a callback log_cb and the log level log_lvl, the first is stored on the global variable pylog_cb. At the end it will call the C++ init passing the function log_mim. When the C++ code will call the callback log_mim (C++ function pointer) it will call the Python pylog_cb.
This solution to “callback problem” works pretty well, the MIM function allows to avoid the incompatibility between the 2 languages.
On PyClient is missing all the source code about the allocation/deallocation of the object and how to link Python and C++ code cause it is beyond the scope of the post.