跳到主要內容

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.

留言

張貼留言

這個網誌中的熱門文章

得利油漆色卡編碼方式

得利油漆色卡編碼方式 類似 Munsell 色彩系統 ,編碼方式為 HUE LRV/CHROMA 例如 10GY 61/449 ( 色卡 ) 編碼數值 描述 10GY hue ,色輪上從 Y(ellow) 到 G(reen) 區分為 0 ~ 99 ,數值越小越靠近 Y,越大越靠近 G 61 LRV (Light Reflectance Value) 塗料反射光源的比率,數值從 0% ~ 100% ,越高越亮,反之越暗,也可理解為明度 449 chroma 可理解為彩度,數值沒有上限,越高顏色純度 (濃度) 越高 取決於測量儀器,對應至 RGB 並不保證視覺感受相同。 參考資料: 色卡對照網站 e-paint.co.uk Written with StackEdit .

UTF8 與 Unicode 的轉換 (C++)

UTF8 與 Unicode 的轉換 (C++) 先釐清一下這兩者的性質 Unicode: 為世界上所有的文字系統制訂的標準,基本上就是給每個字(letter)一個編號 UTF-8: 為 unicode 的編號制定一個數位編碼方法 UTF-8 是一個長度介於 1~6 byte 的編碼,將 unicode 編號 (code point) 分為六個區間如下表 1 Bits First code point Last code point Bytes Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 7 U+0000 U+007F 1 0xxxxxxx 11 U+0080 U+07FF 2 110xxxxx 10xxxxxx 16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx 21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 26 U+200000 U+3FFFFFF 5 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 31 U+4000000 U+7FFFFFFF 6 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 觀察上面的表應該可以發現 除了 7 bits 的區間外,第一個 byte 開頭連續 1 的個數就是長度,例如 110XXXXX 就是 2 byte 長,而 1110xxxx 就是 3 byte 除了第一個 byte 外,之後的 byte 前兩個 bit 一定是 10 開頭,這樣的好處在於確立了編碼的 self-synchronizeing,意即當編碼為多個 byte 時,任取一個 byte 無法正常解碼。 Note 第一點中的例外 (7 bits) 是為了與 ASCII 的相容性,而第二點會影響到 code point 至 UTF-8 的轉換。 為了與 UTF-16 的相容性,在 R...

C++17 新功能 try_emplace

C++17 新功能 try_emplace 回顧 emplace 大家的好朋友 Standard Template Library (STL) 容器提供如 push_back , insert 等介面,讓我們塞東西進去; C++11 之後,新增了 emplace 系列的介面,如 std::vector::emplace_back , std::map::emplace 等,差異在於 emplace 是在容器內 in-place 直接建構新元素,而不像 push_back 在傳遞參數前建構,下面用實例來說明: struct Value { // ctor1 Value ( int size ) : array ( new char [ size ] ) , size ( size ) { printf ( "ctor1: %d\n" , size ) ; } // ctor2 Value ( const Value & v ) : array ( new char [ v . size ] ) , size ( v . size ) { printf ( "ctor2: %d\n" , size ) ; memcpy ( array . get ( ) , v . array . get ( ) , size ) ; } private : std :: unique_ptr < char [ ] > array ; int size = 0 ; } ; struct Value 定義了自訂建構子 (ctor1),以指定大小 size 配置陣列,複製建構子 (ctor2) 則會配置與來源相同大小及內容的陣列,為了方便觀察加了一些 printf 。當我們如下使用 std::vector::push_back 時 std :: vector < Value > v ; v . push_back ( Value ( 2048 ) ) ; 首先 Value 會先呼叫 ctor1,傳給 push_ba...