背景
如果對 callback 已經很熟悉的可以跳過這段;不少函式庫的 API 會接收 callback 或者是 action 函式,如下
// File: callback.hpp
#ifndef CALLBACK_HPP_
#define CALLBACK_HPP_
// void* 為使用者傳入的 context, val 是函式庫指定的值
typedef void(raw_cb_t)(void* ctx, int val);
void libfunc(raw_cb_t *cb, void *ctx);
#endif
這裡的 raw_cb_t cb
就是 callback/action 函式,作為一個參數傳遞給 libfunc
,由它決定何時呼叫,如下
// File: callback.cpp
#include "callback.hpp"
void libfunc(raw_cb_t *cb, void *ctx)
{
for(int i=0; i<10; i++) {
cb(ctx, i);
}
}
使用 libfunc
這個 API 的方式在 C/C++ 裡面可以是
// File: main.cpp
#include <iostream>
#include "callback.hpp"
void handler(void *ctx, int val)
{
int *sum = static_cast<int*>(ctx);
std::cout << (*sum) += val << std::endl;
}
int main(void)
{
int sum = 0;
libfunc(&handler, &sum);
return 0;
}
如果我們想在 Python 裡面使用這個 libfunc 的功能要怎麼達成呢? 比如說想要用 Python 這麼寫
# !/usr/bin/env python
# File: main.py
import pycallback
sum = 0
def handler(int val):
global sum
sum += val
print sum
pass
pycallback.libfunc(handler)
Boost.Python
因為對 Python 來說,函式是一個物件,而不像在 C 中是一個位址,必須做一些包裝/轉發才能夠將兩邊銜接起來;Boost.Python 提供了一個方便的工具 boost::python::call() ,可以簡單地達成這個工作,不過還是需要加上一些型別轉換的包裝;首先是 libfunc
本身的包裝
void wrap_libfunc(PyObject *callable)
{
libfunc(&wrap_callback, callable);
}
這個包裝的目的是把從 python 收到的函式 callable
當成 ctx
交給 libfunc
,等到 libfunc
回呼時再轉型回 PyObject *
並呼叫,因此我們還需要 wrap_callback
來處理回呼後的動作
void wrap_callback(void *ctx, int val)
{
auto callable = static_cast<PyObject*>(ctx);
// PyGILState_STATE state = PyGILState_Ensure();
boost::python::call<void>(callable, val);
// PyGILState_Release(state);
}
可以發現除了轉型與呼叫外,還多了對 GIL 的鎖定 (被註解掉的部分),這邊以註解呈現是因為我們的範例並沒有在 C/C++ 函式庫中建立執行緒,並不需要再鎖定一次 GIL。
加上模組定義的完整程式碼
#include <boost/python.hpp>
#include "callback.hpp"
void wrap_callback(void *ctx, int val)
{
auto callable = static_cast<PyObject*>(ctx);
// PyGILState_STATE state = PyGILState_Ensure();
boost::python::call<void>(callable, val);
// PyGILState_Release(state);
}
void wrap_libfunc(PyObject *callable)
{
libfunc(&wrap_callback, callable);
}
BOOST_PYTHON_MODULE(pycallback)
{
using namespace boost::python;
def("libfunc", wrap_libfunc);
}
函式類別 (Functor)
在 Python 裡面的函數類別有 lambda 與實作 __call__
介面的類別兩種,由於 boost::python::call<> 實作其實就是 PyEval_CallFunction
,因此這兩種類別也都能使用。
# __call__
import pycallback
class handler2(object):
def __init__(self):
self.sum = 0
pass
def __call__(self, val):
self.sum += val
print self.sum
pycallback.libfunc(handler2())
lambda 本身有一些限制,下面的 code 就簡化成印出 callaback value
# lambda
from __future__ import print_function
import pycallback
pycallback.libfunc(lambda x: print(x))
泛化 (Generic)
不同的 callback 介面就要做一分包裝似乎有違 RD 懶惰的天性,不如藉助 C++11 的可變樣版參數來省點打字時間
// SFINAE
template<typename F>
struct wrapper;
// Specialization for void return type
template<typename ... Args>
struct wrapper<void(void*, Args...)>
{
static void call(void* ctx, Args... args)
{
auto callable = static_cast<PyObject*>(ctx);
PyGILState_STATE state = PyGILState_Ensure();
boost::python::call<void>(callable, args...);
PyGILState_Release(state);
}
};
// Specialization for other return types
template<typename R, typename ... Args>
struct wrapper<R(void*, Args...)>
{
static R call(void* ctx, Args... args)
{
R res;
auto callable = static_cast<PyObject*>(ctx);
PyGILState_STATE state = PyGILState_Ensure();
res = boost::python::call<R>(callable, args...);
PyGILState_Release(state);
return res;
}
};
以上幾個函式樣版用來幫忙產生出需要的 callback 介面,用法如下
void wrap_libfunc(PyObject *callable)
{
libfunc(&wrapper<raw_cb_t>::call, callable);
}
如此一來,假設有個 another_libfunc()
想要使用的 callback 定義為
typedef int(another_callback_t)(void* ctx, std::string val);
我們就依樣畫葫蘆寫個
int wrap_another_libfunc(PyObject *callable)
{
another_libfunc(&wrapper<another_callback_t>::call, callable);
}
打完收功。
不過 wrap_xxx_libfuc
本身就很難這樣搞了,因為這裡多了一個要記住的函數指標,也就是一個物件狀態,無法單純包一個(非物件)函式,除非要求 Python 那邊傳入的是一個由 C++ 這邊規定的 Visitor 來套招,但如此一來,不免額外增加客戶端 (Python) 使用上的限制,更何況目前為止都還沒處理 Python 的 __call__
,所以泛化就此打住。
Note
前面的 callback typedef 不是使用一般常見的函數指標 e.gtypedef void(*fn_ptr)()
而是使用函數型別,i.e.typedef void(fn)()
,這是為了方便在 wrapper 時使用,不這麼做也可以利用std::remove_pointer
在 wrapper 做手腳。
使用 CMake 編譯
#File: CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
find_package(Boost REQUIRED python)
find_package(PythonLibs REQUIRED)
include_directories (${Boost_INCLUDE_DIRS} ${PYTHON_INCLUDE_DIRS})
# XXX This is just an example, so we compile they as a single library
add_library(pycallback SHARED callback.cpp pycallback.cpp)
target_link_libraries (pycallback pthread ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})
# XXX Remove prefix due to module init function generated by boost.python does not contain "lib"
set_target_properties(pycallback PROPERTIES PREFIX "")
結論
以上講的 callback 主要是 C callback,一些比較現代的 C++ 函式庫可能會使用 std::function 及 lambda 來傳遞 callback ,提供較好的彈性。因此,本文的應用範疇在於包裝純 C API 的函式庫,或者是比較保守的 C++ 函式庫 (其實數量上多數)。
Written with StackEdit.
太神啦,看完頭好痛啊!!
回覆刪除