上一篇文章介绍了使用 CMake 构建 Python C/C++ 扩展的两个方案,也提到了之前在工作中做这个的主要目的是为了能够使用 abseil-cpp 的 Swiss Tables 优化性能,那么这篇文章就来简单介绍一下如何在 Cython 项目中使用 abseil-cpp.
项目目录结构
$ tree -L 2 .
.
├── CMakeLists.txt # 项目主 CMake 配置
├── absl.pxd # abseil-cpp 接口的部分 Cython 定义文件
├── hello # hello Python 库
│ ├── CMakeLists.txt # Python C++ 扩展 CMake 配置
│ ├── __init__.py
│ └── _hello.pyx # Cython 文件
├── pyproject.toml
├── setup.py
├── src
│ ├── CMakeLists.txt # 项目 C/C++ 代码 CMake 配置
│ ├── absl.cc
│ └── absl.h
├── test_hello_cython.py # 测试代码
├── vendor # C/C++ 第三方库放置路径
│ └── abseil-cpp # abseil-cpp Git submodule
└── venv # Python virtualenv
该示例项目的代码托管在 GitHub: https://github.com/messense/cython-abseil-sample
接入 abseil-cpp
可以直接把 abseil-cpp 作为一个 Git submodule 导入到自己的项目中
git submodule add --depth 50 https://github.com/abseil/abseil-cpp.git vendor/abseil-cpp
这里我把它放到了 vendor/abseil-cpp
目录中,然后可以在项目主 CMakeLists.txt
文件中配置依赖,官方文档 上有更详细的说明。
cmake_minimum_required(VERSION 3.5.0)
project(hello)
set (CMAKE_POSITION_INDEPENDENT_CODE ON)
# Abseil requires C++11
set(CMAKE_CXX_STANDARD 11)
# Process Abseil's CMake build system
add_subdirectory(vendor/abseil-cpp)
include_directories(
/usr/local/include
${CMAKE_CURRENT_SOURCE_DIR}/vendor/abseil-cpp
${CMAKE_CURRENT_SOURCE_DIR}/src)
add_subdirectory(src)
add_subdirectory(hello)
上面的配置把 src/
和 hello/
目录也作为 CMake 子目录加入了配置,下面分别介绍一下。
src 目录 - C/C++ 模块
src 目录主要存放了该项目点 C/C++ 代码,这部分代码和是 Python 无关的。
CMakeLists.txt
set(SOURCES absl.cc)
add_library(hello_absl STATIC ${SOURCES})
target_link_libraries(hello_absl absl::flat_hash_map)
这里的构建配置非常简单,只是构建了一个静态的 hello_absl
target 并链接 absl::flat_hash_map
库,这个 target 暴露的 public API 只有一个返回值类型为 absl::flat_hash_map
的函数 fn_return_flat_hash_map
,定义在 src/absl.h
文件中:
#include <string>
#include "absl/container/flat_hash_map.h"
absl::flat_hash_map<int, std::string> fn_return_flat_hash_map(void);
其实现也非常简单 absl.cc
:
#include "absl.h"
absl::flat_hash_map<int, std::string> fn_return_flat_hash_map(void) {
absl::flat_hash_map<int, std::string> numbers =
{{1, "one"}, {2, "two"}, {3, "three"}};
return numbers;
}
hello 目录 - Cython 模块
hello 目录存放的是 Python 模块代码,包含一个 _hello.pyx
的 Cython 模块。
CMakeLists.txt
if(SKBUILD)
message(STATUS "The project is built using scikit-build")
find_package(PythonExtensions REQUIRED)
find_package(Cython REQUIRED)
include_directories(
/usr/local/include
${PYTHON_INCLUDE_DIRS}
${CMAKE_CURRENT_SOURCE_DIR}/vendor/abseil-cpp)
add_cython_target(_hello CXX)
add_library(_hello MODULE ${_hello})
target_link_libraries(_hello hello_absl)
python_extension_module(_hello)
install(TARGETS _hello LIBRARY DESTINATION hello)
endif()
- 增加
SKBUILD
变量的判断,只有在使用 scikit-build 构建的时候才会去构建 Python 扩展,这样如果你的项目中的 C/C++ 部分不依赖 Python 也可以运行、测试或者 benchmark 的话,会比较方便 - 增加了
find_package(Cython REQUIRED)
和add_cython_target(_hello CXX)
配置 Cython 模块支持
_hello.pyx
from libcpp.string cimport string
from absl cimport flat_hash_map
cdef extern from "absl.h":
cdef flat_hash_map[int, string] fn_return_flat_hash_map() nogil
cpdef dict hello_absl():
cdef flat_hash_map[int, string] a_map
with nogil:
a_map = fn_return_flat_hash_map()
return {it.first: it.second.decode() for it in a_map}
- 从 Cython 自带的
libcpp
标准库导入了std::string
- 从我们定义的
absl.pxd
导入了flat_hash_map
,absl.pxd
详情下面再解释 - 使用
cdef extern
定义了上述src
目录中 C++ 模块定义的fn_return_flat_hash_map
函数原型 - 在
cpdef dict hello_absl
函数中调用fn_return_flat_hash_map
C++ 函数并转换成 Python dict 返回
这里有两个问题需要解答:
from absl cimport flat_hash_map
如何工作的?flat_hash_map
转换成 Python dict 为什么不能像libcpp
的unordered_map
一样直接赋值给 dict 类型的变量自动转换而需要迭代手动生成一个 dict?
问题 1 我们需要了解一下 Cython 是如何调用 C/C++ 函数的,官方文档中关于 pxd 文件的文档有如下解释
Cython uses
.pxd
files which work like C header files – they contain Cython declarations (and sometimes code sections) which are only meant for inclusion by Cython modules. A pxd file is imported into a pyx module by using the cimport keyword.
为什么 from libcpp.string import string
不需要我们提供 pxd 文件呢?这是因为 Cython 已经默认包含了 libcpp 的接口定义,但是 abseil-cpp 的接口定义 Cython 默认是没有的,所以需要提供我们,因为我们这里只用到了 abseil-cpp 的 flat_hash_map
类,故 absl.pxd
中目前只有它的定义,如果以后用到其他的类,也可以在里面增加定义。
absl.pxd
absl.pxd
的内容是从 Cython 自带的 libcpp
代码中的 unordered_map.pxd
代码修改的,diff 一下这两个文件
--- unordered_map.pxd2020-05-10 16:29:28.000000000 +0800
+++ absl.pxd2020-05-10 14:54:27.000000000 +0800
@@ -1,7 +1,7 @@
-from .utility cimport pair
+from libcpp.utility cimport pair
-cdef extern from "<unordered_map>" namespace "std" nogil:
- cdef cppclass unordered_map[T, U]:
+cdef extern from "absl/container/flat_hash_map.h" namespace "absl" nogil:
+ cdef cppclass flat_hash_map[T, U]:
ctypedef T key_type
ctypedef U mapped_type
ctypedef pair[const T, U] value_type
@@ -21,17 +21,17 @@
pass
cppclass const_reverse_iterator(reverse_iterator):
pass
- unordered_map() except +
- unordered_map(unordered_map&) except +
- #unordered_map(key_compare&)
+ flat_hash_map() except +
+ flat_hash_map(flat_hash_map&) except +
+ #flat_hash_map(key_compare&)
U& operator[](T&)
- #unordered_map& operator=(unordered_map&)
- bint operator==(unordered_map&, unordered_map&)
- bint operator!=(unordered_map&, unordered_map&)
- bint operator<(unordered_map&, unordered_map&)
- bint operator>(unordered_map&, unordered_map&)
- bint operator<=(unordered_map&, unordered_map&)
- bint operator>=(unordered_map&, unordered_map&)
+ #flat_hash_map& operator=(flat_hash_map&)
+ bint operator==(flat_hash_map&, flat_hash_map&)
+ bint operator!=(flat_hash_map&, flat_hash_map&)
+ bint operator<(flat_hash_map&, flat_hash_map&)
+ bint operator>(flat_hash_map&, flat_hash_map&)
+ bint operator<=(flat_hash_map&, flat_hash_map&)
+ bint operator>=(flat_hash_map&, flat_hash_map&)
U& at(const T&)
const U& const_at "at"(const T&)
iterator begin()
@@ -43,8 +43,8 @@
const_iterator const_end "end"()
pair[iterator, iterator] equal_range(T&)
pair[const_iterator, const_iterator] const_equal_range "equal_range"(const T&)
- iterator erase(iterator)
- iterator erase(iterator, iterator)
+ void erase(iterator)
+ void erase(iterator, iterator)
size_t erase(T&)
iterator find(T&)
const_iterator const_find "find"(T&)
@@ -60,7 +60,7 @@
reverse_iterator rend()
const_reverse_iterator const_rend "rend"()
size_t size()
- void swap(unordered_map&)
+ void swap(flat_hash_map&)
iterator upper_bound(T&)
const_iterator const_upper_bound "upper_bound"(T&)
#value_compare value_comp()
可以发现,除了将 unordered_map
名字替换成 flat_hash_map
和修改 extern from
路径外,只有几个 erase
方法的返回值类型不一样,unordered_map.erase
会返回一个新的 iterator
而 flat_hash_map.erase
返回值类型为 void
,这是因为 abseil-cpp 的 flat_hash_map
为了优化 erase 方法的性能和 C++ STL 的 unordered_map
erase 方法行为有所不同导致的:
Guarantees an `O(1)`` erase method by returning void instead of an iterator.
问题 2 则是因为 Cython 编译器对 libcpp 有特殊支持会生成很多转换函数,而我们自行提供的 .pxd
文件则没有这种特殊待遇导致的。有兴趣的可以看一下 Cython.Compiler.PyrexTypes
模块中的 CTypedefType.create_to_py_utility_code 方法代码。
本文只给出了一个 C++ 代码返回值类型为 abseil-cpp 类型的例子,实际上也可以扩展为函数参数类型为 abseil-cpp 或者其他任何 C/C++ 第三方库的类型,这里不再赘述。
测试
test_hello_cython.py
文件中编写了一个 test_hello_absl
测试用例:
import hello
def test_hello_absl():
ret = hello.hello_absl()
assert ret == {1: "one", 2: "two", 3: "three"}
运行 pytest -v
可以确认测试正常:
$ pytest -v
test_hello_cython.py::test_hello_absl PASSED