假定我有一个用 C++ 创建一个的动态库,该如何在 Python 下调用它。
- ctypes?
- cffi?
- pybind11?
- sip?
- shiboken?
- ...
每一个东西都有复杂的细节,我们用最简单的例子,简单过一下,不发散。
为保证例子在 Windows、Linux 和 MacOS 下都能工作,
C++部分统一使用了cmake,并借助 GitHub 的 CI 功能进行自动化测试。本文涉及到的完整源码均位于 github仓库。
准备工作
先准备一下目录结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | project/
├── cpp/
│ ├── CMakeLists.txt
│ ├── lib1/
│ │ ├── lib1.cpp
│ │ ├── lib1.h
│ │ ├── CMakeLists.txt
│ ├── lib2/
├── bindings/
│ ├── lib2_pythonapi/
│ ├── lib2_pybind11/
│ ├── lib2_sip/
│ ├── lib2_shiboken/
├── pythontests/
│ ├── test_lib1_ctypes.py
│ ├── test_lib1_cffi.py
│ ├── test_lib2_pythonapi.py
│ ├── test_lib2_pybind11.py
│ ├── test_lib2_sip.py
│ ├── test_lib2_shiboken.py
├── .github/
│ ├── workflows/
│ ├── ci.yml
|
为了简单起见,通过配置cpp/CMakeLists.txt
,将所有动态库统一放置到构建目录下的bin或lib目录下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | cmake_minimum_required(VERSION 3.22)
project(CppLibs)
if (WIN32)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # For .dll
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # For .lib
else()
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # For .so/.dylib
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # For .a
endif()
add_subdirectory(lib1)
#add_subdirectory(lib2)
|
c++ 动态库(C接口)
动态库内容,就定一个简单的加法函数吧:
| #include "lib1.h"
int add(int a, int b) {
return a + b;
}
|
头文件cpp\lib1\lib1.h
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | #pragma once
#ifdef _WIN32
#ifdef BUILD_LIB1_DLL
#define LIB1_DLL_EXPORT __declspec(dllexport)
#else
#define LIB1_DLL_EXPORT __declspec(dllimport)
#endif
#else
#define LIB1_DLL_EXPORT
#endif
extern "C" {
LIB1_DLL_EXPORT int add(int a, int b);
}
|
工程文件cpp/lib1/CMakeLists.txt
:
| add_library(lib1 SHARED lib1.cpp)
if (WIN32)
target_compile_definitions(lib1 PRIVATE BUILD_LIB1_DLL)
endif()
|
c++ 动态库(C++接口)
lib2的定义和前面lib1基本完全一样,只是没有用 extern "C"
,头文件如下
1
2
3
4
5
6
7
8
9
10
11
12
13 | #pragma once
#ifdef _WIN32
#ifdef BUILD_LIB2_DLL
#define LIB2_DLL_EXPORT __declspec(dllexport)
#else
#define LIB2_DLL_EXPORT __declspec(dllimport)
#endif
#else
#define LIB2_DLL_EXPORT
#endif
LIB2_DLL_EXPORT int add(int a, int b);
|
直接使用动态库
如果动态库导出的是C接口(比如lib1),可以直接使用动态库。
对于lib2,由于C++有名字改编的问题,尽管只要我们知道改编后的名字,也是可以这么用的。但不同C++编译器名字改编规则也不同,偶尔应急用一下还行,折腾下去意义不大。
通过 ctypes
对于C接口的lib1,可以直接使用Pythoni标准库中的ctypes。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | import ctypes
# Load the shared library
try:
lib1 = ctypes.CDLL(lib_full_path)
print(f"Successfully loaded library from {lib_full_path}")
except OSError as e:
print(f"Failed to load library: {e}")
sys.exit(1)
# Specify the function signature
lib1.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib1.add.restype = ctypes.c_int
# Test the function
def test_lib1_add():
result = lib1.add(1, 1)
print(f"lib1.add(1,1) = {result}")
assert result == 2
if __name__ == "__main__":
test_lib1_add()
|
本身ctypes使用还算好,不过不同平台下名字不一样,还是需要注意的:
| # Determine the shared library name
lib_base_name = "lib1"
if sys.platform == "win32":
lib_path = f"bin/{lib_base_name}.dll"
elif sys.platform == "darwin":
lib_path = f"lib/lib{lib_base_name}.dylib"
else:
lib_path = f"lib/lib{lib_base_name}.so"
lib_full_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../build", lib_path))
|
通过 cffi
对C接口的lib1,使用cffi也简单,test_lib1_cffi.py
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | from cffi import FFI
# Create an FFI instance
ffi = FFI()
# Define the C function signature
ffi.cdef("""
int add(int a, int b);
""")
lib1 = ffi.dlopen(lib_full_path)
# Test the function
def test_lib1_add():
result = lib1.add(1, 1)
print(f"lib1.add(1,1) = {result}")
assert result == 2
if __name__ == "__main__":
test_lib1_add()
|
通过创建绑定使用
这个方式就比较多了,可以直接根据Python的扩展要求手撸出来一个(针对简单例子)。更多还是借助其他工具方便
直接使用Python API
最直接的方式,但写起来也最繁琐
测试用例
都封装成模块了,用例简单:
| sys.path.insert(0, mycommon.get_module_path())
import lib2_pythonapi
# Test the function
def test_lib2_add():
result = lib2_pythonapi.add(1, 1)
print(f"lib2_pythonapi.add(1,1) = {result}")
assert result == 2
if __name__ == "__main__":
test_lib2_add()
|
创建绑定
直接定义一个模块lib2_pythonapi
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | #define PY_SSIZE_T_CLEAN
#include <Python.h>
#include "../../cpp/lib2/lib2.h"
static PyObject* py_add(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
return NULL;
}
int result = add(a, b); // call functions from lib2
return PyLong_FromLong(result);
}
static PyMethodDef ModuleMethods[] = {
{"add", py_add, METH_VARARGS, "Add two numbers"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT,
"lib2_pythonapi",
"A Python module that links to lib2.dll/liblib2.so",
-1,
ModuleMethods
};
PyMODINIT_FUNC PyInit_lib2_pythonapi(void) {
return PyModule_Create(&moduledef);
}
|
工程文件:
1
2
3
4
5
6
7
8
9
10
11
12 | find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
add_library(lib2_pythonapi MODULE lib2_pythonapi.cpp)
target_link_libraries(lib2_pythonapi PRIVATE lib2 Python3::Python)
if (WIN32)
set_target_properties(lib2_pythonapi PROPERTIES SUFFIX ".pyd") # Windows generate .pyd
endif()
set_target_properties(lib2_pythonapi PROPERTIES
PREFIX "" # Remove prefix
)
|
使用 pybind11
先看看测试用例,由于是将lib2封装成了lib2_pybind11,可以直接导入:
| sys.path.insert(0, mycommon.get_module_path())
import lib2_pybind11
# Test the function
def test_lib2_add():
result = lib2_pybind11.add(1, 1)
print(f"lib2_pybind11.add(1,1) = {result}")
assert result == 2
if __name__ == "__main__":
test_lib2_add()
|
pybind11 安装
pybind11 是受 Boost.Python 启发创建的,但没有Boost那么笨重。
pybind11仅有头文件构成,可以将其包含在项目中,或者通过pip安装
安装后需要配置环境变量让cmake能够能找到它,它的路径在:
| python -m pybind11 --cmakedir
|
创建 pybind11 绑定
源文件:
| #include <pybind11/pybind11.h>
#include "../../cpp/lib2/lib2.h"
PYBIND11_MODULE(lib2_pybind11, m) {
m.doc() = "A simple example";
m.def("add", &add, "A function that adds two numbers");
}
|
工程文件
| set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 REQUIRED)
pybind11_add_module(lib2_pybind11 lib2_pybind11.cpp)
target_link_libraries(lib2_pybind11 PRIVATE lib2 pybind11::module)
|
使用 SIP
SIP 是 PyQt使用的绑定工具,开发始于1998年。不用于PyQt采用GPL和商业双授权,SIP采用BSD协议,无需担心协议问题。
在 SIP 6.x 中,传统的 sip 命令被替换为 sip-build、sip-module 等一组命令集合。
安装
这是一个重量级的东西。不过这东西20多年了,可参考的文档实在太少。可能学习能力退化了,感觉比其10年前的老版本难用!
创建绑定
准备一个.sip
文件,取名lib_sip.sip
:
| %Module(name=lib2_sip, language="C++")
%ModuleHeaderCode
#include "lib2.h"
%End
int add(int a, int b);
|
创建一个工程文件pyproject.toml
:
1
2
3
4
5
6
7
8
9
10
11
12 | [build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"
[project]
name = "lib2_sip"
[tool.sip.bindings.lib2_sip]
headers = ["../../cpp/lib2/lib2.h"]
include-dirs = ["../../cpp/lib2"]
libraries = ["lib2"]
library-dirs = ["../../build/lib", "../../build/lib/Release"]
|
直接执行
如果有疑难杂症,记得加上选项 --verbose
以便于排查。
即可生成绑定模块:
- Windows下:
build\lib2_sip\build\lib.win-amd64-cpython-312\lib2_sip.cp312-win_amd64.pyd
- Ubuntu下:
build/lib.linux-x86_64-cpython-312/lib2_sip.cpython-312-x86_64-linux-gnu.so
- MacOS下:
build/lib.macosx-10.13-universal2-cpython-312/lib2_sip.cpython-312-darwin.so
单元测试
测试代码本身简单,不过找到这个模块,需要操作一下。
| import lib2_sip
# Test the function
def test_lib2_add():
result = lib2_sip.add(1, 1)
print(f"lib2_sip.add(1,1) = {result}")
assert result == 2
if __name__ == "__main__":
test_lib2_add()
|
使用 Shiboken
Shiboken是PySide使用的为C++Qt库创建绑定的工具。开发始于2008年的Nokia,2016年复活,当前版本是其复活后的Shiboken6。
PySide项目最开始阶段使用的是 Boost.Python,因为性能问题,才决定自行开发Shiboken。
注意,可执行程序Shiboken位于软件包 shiboken6-generator
中,软件包shiboken6
只是其运行库。
安装
| pip install shiboken6 shiboken6-generator
|
另外,这个东西还依赖libclang,(shiboken6需要libclang10。)需要手动安装。环境变量 CLANG_INSTALL_DIR
也需要指向它。
准备绑定
它需要一个类型系统文件,取名 lib2_shiboken.xml
:
| <?xml version="1.0" encoding="UTF-8"?>
<typesystem package="lib2_shiboken">
<primitive-type name="int"/>
<function signature="add(int, int)" />
</typesystem>
|
此时,执行shiboken6即可生成所需的cpp文件:
| shiboken6 ..\..\cpp\lib2\lib2.h .\lib2_shiboken.xml
|
生成文件位于.\out\lib2_shiboken\lib2_shiboken_module_wrapper.cpp
由于 Shiboken没有提供CMake支持,如果不跨平台的话,直接编译这个文件可能是最简单的。不然,它仓库中最简单的例子samplebinding也足以劝退大多数人。
构建
准备工程文件CMakeLists.txt:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 | set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Find Python3 interpreter and development components
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
# Debug: Print Python3 executable path
message("Python3_EXECUTABLE: ${Python3_EXECUTABLE}")
# Attempt to retrieve the Shiboken6 installation path dynamically
execute_process(
COMMAND python -c "import shiboken6; print(shiboken6.__path__[0])"
OUTPUT_VARIABLE SHIBOKEN6_PATH
ERROR_VARIABLE SHIBOKEN6_ERROR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# Debug: Print the result of execute_process
message("SHIBOKEN6_PATH: ${SHIBOKEN6_PATH}")
message("SHIBOKEN6_ERROR: ${SHIBOKEN6_ERROR}")
# Set paths for Shiboken6 tool, headers, and library
set(shiboken_include_dir ${SHIBOKEN6_PATH}/../shiboken6_generator/include)
message("shiboken include path: " ${shiboken_include_dir})
# Configure platform-specific library settings
if(WIN32)
set(shiboken_library ${SHIBOKEN6_PATH}/shiboken6.abi3.lib)
set(module_suffix ".pyd")
elseif(APPLE)
set(shiboken_library ${SHIBOKEN6_PATH}/libshiboken6.abi3.dylib)
set(module_suffix ".so")
else()
set(shiboken_library ${SHIBOKEN6_PATH}/libshiboken6.abi3.so)
set(module_suffix ".so")
endif()
# Specify the C++ header and typesystem file for the binding
set(wrapped_header ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp/lib2/lib2.h)
set(typesystem_file ${CMAKE_CURRENT_SOURCE_DIR}/lib2_shiboken.xml)
# Use Shiboken6 to generate the binding source code
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/lib2_shiboken/lib2_shiboken_module_wrapper.cpp
COMMAND shiboken6
--output-directory=${CMAKE_CURRENT_BINARY_DIR}
--include-paths=${CMAKE_CURRENT_SOURCE_DIR}/../../cpp/lib2
--typesystem-paths=${shiboken_include_dir}
${wrapped_header}
${typesystem_file}
DEPENDS lib2 ${wrapped_header} ${typesystem_file}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
# Create the Python module
add_library(lib2_shiboken MODULE
${CMAKE_CURRENT_BINARY_DIR}/lib2_shiboken/lib2_shiboken_module_wrapper.cpp
)
# Link the Shiboken6 runtime library and include the headers
target_include_directories(lib2_shiboken PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp/lib2 ${shiboken_include_dir})
target_link_libraries(lib2_shiboken PRIVATE
lib2
${shiboken_library}
Python3::Python
)
# Set the module properties
set_target_properties(lib2_shiboken PROPERTIES
PREFIX "" # Remove default 'lib' prefix
SUFFIX ${module_suffix} # Use platform-specific suffix
)
|
参考
- https://docs.python.org/3/library/ctypes.html
- https://docs.python.org/3/extending/extending.html
- https://cffi.readthedocs.io/en/stable/
- https://pybind11.readthedocs.io/en/latest/
- https://pypi.org/project/sip/
- https://doc.qt.io/qtforpython-6/shiboken6/gettingstarted.html
- https://www.kdab.com/creating-python-bindings-for-qt-libraries/
- https://github.com/qtproject/pyside-pyside-setup