本文的目的是探索一种从Numba 编译的函数中调用C++库函数的方法------用numba.jit(nopython=True) 。
虽然存在将C++代码包裹到Python中的方法,但从Numba编译的函数中调用这些包裹器往往不像人们希望的那样直接和高效。这种低效率的根本问题在于,Python/C++包装器被设计为在Python对象上调用。例如,考虑到这篇文章的目的,对 Numba 编译函数的调用将涉及到将 Python 对象转换为低级别的对象,例如,转换为一些 C/C++ 相当的内在类型或结构,然后再将其转换为 Python 对象并传递给 Python/C++ 封装函数。然后这个包装函数将Python对象(再次)转换为一个等效的C/C++类型的对象,最后可以传递给底层的C++库函数。C++函数的返回值也将被多次转换,只是与函数的调用序列方向相反。总的来说,所有这些对象的转换可能会在从 Python 调用原本高效的 C++ 库函数时产生相当大的开销。
从 Numba 调用 C++ 库函数的另一个困难来自于 C++ 编译器对函数名的处理,以支持函数/方法重载以及其它相关的 C++ 语言特性。原则上,如果知道C++编译器如何在内部转换函数名,就可以从任何Numba编译的函数中直接调用这样的C++库函数(使用numba.cfunc功能)。然而,名字的变换算法是依赖于C++编译器厂商的,在实践中,很难以可移植的方式预测变换后的名字。
cxx2py.py工具
在这篇文章中,我们将介绍一种直接有效的方法,从Numba编译的函数中调用C++库函数,以规避名称杂乱的问题。这个方法的基础是在运行时确定C++库函数的地址,然后与函数签名一起用来建立一个高效的调用序列。这种方法需要创建一个小型的C/C++包装库,其中包含export "C"-归属的函数,这些函数返回C++库函数的地址,并且可以使用各种技术从Python中轻松调用,这里我们使用ctypes。
我们提供了一个Python脚本cxx2py.py,它可以从用户提供的C++头文件和源文件中自动生成C/C++包装库以及Pythonctypes包装模块。Python模块包含ctypes C++库函数的定义,Numba编译的函数能够直接调用,而不需要上面提到的昂贵和多余的对象转换。在Python包装模块中使用ctypes 的一个替代方法是使用Numba包装地址协议--WAP。如果ctypes的 "C++表现力 "被证明是不够的,应该考虑使用它。
目前,cxx2py.py 工具中支持的功能包括:
- 包装具有标量输入和返回值的C++库函数。
- 支持可以在C++命名空间内定义的C++函数。
- 支持可能是静态类成员函数的C++函数。
cxx2py.py 工具可以被扩展以支持其他的C++特性,例如:
- 从 Python 创建 C++ 类/结构实例。
- 在不同的语言之间来回传递C++类/结构实例。
- 调用C++类/结构实例的方法。
- 支持指针类型作为函数输入和返回值。
- 等等。
cxx2py.py 工具使用clang++ 来解析C++头文件并建立C/C++包装共享库。它还使用RBC--远程后端编译器来解析C++函数的签名,以方便使用。
例子
作为先决条件,让我们创建一个Conda环境,如下所示
$ conda create -n cxx2py-demo -c conda-forge numba rbc cxx-compiler clangdev
$ conda activate cxx2py-demo
我们假设cxx2py.py脚本被复制到了当前工作目录中,并且是有效的。
$ python cxx2py.py --help
usage: cxx2py.py [-h] [-m MODULENAME] [--clang-exe CLANG_EXE]
[--clang-ast-dump-flags CLANG_AST_DUMP_FLAGS]
[--clang-build-flags CLANG_BUILD_FLAGS]
[--clang-extra-flags CLANG_EXTRA_FLAGS]
[--build] [--verbose]
file [file ...]
Generate ctypes wrappers to C++ library functions
positional arguments:
file C++ header/source file
optional arguments:
-h, --help show this help message and exit
-m MODULENAME, --modulename MODULENAME
Python module name of ctypes wrappers (default: untitled)
--clang-exe CLANG_EXE
Path to clang compiler (default: clang++)
--clang-ast-dump-flags CLANG_AST_DUMP_FLAGS
Override flags to clang ast dump command (default:
'-Xclang -ast-dump -fsyntax-only -fno-diagnostics-color')
--clang-build-flags CLANG_BUILD_FLAGS
Override flags to clang build shared library command
(default: '-shared -fPIC')
--clang-extra-flags CLANG_EXTRA_FLAGS
Extra flags to clang command (default: '')
--build Build shared library (default: False)
--verbose Be verbose (default: False)
接下来,让我们考虑下面这个C++头文件和源文件,我们将把它作为一个C++库的模型。
/* File: foo.hpp */
#include <iostream>
int foo(int a);
namespace ns {
namespace ns2 {
double bar(double x);
class BarCls {
public:
BarCls(double a): a_(a) {}
double get_a() { return a_; }
static int fun() { return 54321; }
private:
double a_;
};
}
}
/* File: foo.cpp */
int foo(int a) {
std::cout << "in foo(" << std::to_string(a) << ")" << std::endl;
return a + 123;
}
namespace ns { namespace ns2 {
double bar(double a) {
std::cout << "in ns::ns2::bar(" << std::to_string(a) << ")" << std::endl;
return a + 12.3;
}
}}
我们建立一个 Python ctypes 封装库,使用
$ python cxx2py.py -m libfoo foo.hpp foo.cpp --build
DONE
As a quick test, try running:
LD_LIBRARY_PATH=. python -c "import untitled as m; print(m.__all__)"
将在当前目录下创建三个文件。
$ ls *libfoo*
cxx2py_libfoo.cpp libcxx2py_libfoo.so libfoo.py
注意,生成的cxx2py_libfoo.cpp文件包含了用于返回 C++ 函数地址的轻量级 C 函数。
#include <memory>
#include <cstdint>
#include "foo.hpp"
extern "C" intptr_t get_foo_address() {
/* int (int) */
return reinterpret_cast<intptr_t>(std::addressof(foo));
}
extern "C" intptr_t get_ns__ns2__bar_address() {
/* double (double) */
return reinterpret_cast<intptr_t>(std::addressof(ns::ns2::bar));
}
extern "C" intptr_t get_ns__BarCls__fun_address() {
/* int () */
return reinterpret_cast<intptr_t>(std::addressof(ns::BarCls::fun));
}
当使用--build 标志时,cxx2py_libfoo.cpp 文件被内置到共享库libcxx2py_libfoo.so 。
让我们在 Python 中测试包装模块libfoo。
$ export LD_LIBRARY_PATH=. # this makes sure that ctypes is able to find the shared library
$ python
>>> import libfoo
>>> libfoo.__all__
['foo', 'ns__ns2__bar', 'ns__BarCls__fun']
>>> libfoo.foo(5)
in foo(5)
128
>>> libfoo.ns__ns2__bar(1.2)
in ns::ns2::bar(1.200000)
13.5
>>> libfoo.ns__BarCls__fun()
54321
也就是说,由于使用了ctypes!,C++ 库函数可以直接从 Python 中调用。
此外,C++库的函数可以从Numba编译的函数中调用。比如说。
>>> import numba
>>> @numba.njit
... def fun(x):
... return libfoo.foo(x + 2)
...
>>> fun(5)
in foo(7)
130
摘要
在这篇文章中,我们概述了一种从Python中调用C++库函数的方法,重点是以最小的开销从Numba编译的函数中使用它们。虽然所提供的工具cxx2py.py目前只支持包装带有标量输入和返回值的 C++ 函数,但它也可以很容易地扩展到支持其他 C++ 特性。