跳到主要內容

boost::python - 從 C C++ 中呼叫 Python 函式

boost::python - 從 C C++ 中呼叫 Python 函式

背景

如果對 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.g typedef 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.

留言

張貼留言