背景
小h上次使用了pybind11来调用c++的方法,这次同样是在项目中遇到了需要在py层调用c++方法的情况,现在对于性能的需求更加敏感,所以需要使用c++的底层方法来获得结果(当然也可能只是因为c++有这个方法,直接拿来用比较方便),选择ctypes应该也是因为这个比较方便吧。
示例
在c++侧,我们需要实现一个函数,然后导出一个动态库方法,这里楼主从py层传入了一个回调函数,返回了对应的字符结果。
在py侧,首先需要使用ctypes.cdll加载刚才编译出的动态库,声明对应的方法,之后实现一个回调函数进行传入。
c++动态库实现
#include <iostream>
#include <string>
// 回调类型:void callback(int, const char*)
using callbacktype = void(*)(int, const char*);
extern "c" {
// 导出函数:循环调用回调
void run_with_callback(callbacktype cb, int count) {
if (!cb) {
std::cerr << "[c++] callback is null" << std::endl;
return;
}
std::cout << "[c++] run_with_callback start, count = " << count << std::endl;
for (int i = 0; i < count; ++i) {
std::cout << "[c++] before calling callback, i = " << i << std::endl;
std::string message = "msg from c++ index = " + std::to_string(i);
cb(i, message.c_str()); // 回调到 python
std::cout << "[c++] after calling callback, i = " << i << std::endl;
}
std::cout << "[c++] run_with_callback end" << std::endl;
}
} // extern "c"ps:c++ 默认会对函数名做 name mangling(名字改编),导致动态库中的符号名变复杂;ctypes按 c abi 去找函数名,如果不显式用 extern "c",python 侧很难直接按名字找到
用cmake编译成动态库
cmake_minimum_required(version 3.10)
project(pyc_callback_demo languages cxx)
add_library(mycallback shared callback_lib.cpp)
set_target_properties(mycallback properties
output_name "mycallback"
cxx_standard 17
cxx_standard_required yes
)构建
mkdir -p build cd build cmake .. cmake --build .
这样就得到了一个.so文件,供py侧加载。
python加载动态库并调用
- 用 @callback_ctype 装饰 python 函数,ctypes 会把它包装成 c 函数指针。
- 字符串参数在 python 中收到的是 bytes,需要手动解码
import ctypes
import pathlib
import os
# 当前文件所在目录
base_dir = pathlib.path(__file__).resolve().parent
lib_path = base_dir / "cpp_lib" / "build" / "libmycallback.so"
if not lib_path.exists():
raise filenotfounderror(f"找不到动态库: {lib_path}. 请先在 cpp_lib 目录下用 cmake 编译生成 libmycallback.so")
# 加载动态库
lib = ctypes.cdll(str(lib_path))
# 定义 c 端回调类型
# 第二个参数用 ctypes.c_char_p(c 端是 const char*)
callback_ctype = ctypes.cfunctype(none, ctypes.c_int, ctypes.c_char_p)
# 声明 c 函数的签名:void run_with_callback(callbacktype cb, int count)
lib.run_with_callback.argtypes = [callback_ctype, ctypes.c_int]
lib.run_with_callback.restype = none
# python 侧的回调实现
@callback_ctype
def py_callback(i: int, msg: bytes) -> none:
# msg 是 bytes,需要按合适编码解码,这里假设 utf-8
text = msg.decode("utf-8") if msg is not none else "<null>"
print(f"[python] in callback, i = {i}, msg = {text}")
def main() -> none:
print("[python] before calling c function")
# 调用 c++ 库函数,并把 python 回调传进去
lib.run_with_callback(py_callback, 3)
print("[python] after calling c function")
if __name__ == "__main__":
main()
ctypes与pybind11区别
ctypes:纯 python 侧绑定
不需要在 c++ 里写任何“绑定代码”,只要提供 c abi 的动态库。
python 侧通过 ctypes.cdll、cfunctype 等描述函数签名。
只要函数满足“c 风格接口”(例如 extern "c" + 基本类型/指针),不在意具体是由 c 还是 c++ 实现。
非常适合:
- 已经有现成 c 库 / c 接口的 c++ 库
- 简单函数调用、回调、不复杂的数据结构
pybind11:在 c++ 侧写绑定,生成 python 模块
是一个头文件库,写法大致像如之前提供的示例一致
编译后得到的是一个 python 扩展模块(.so),import mymodule 就像普通 python 包一样使用。
可以非常自然地暴露:
- c++ 类、方法、构造函数
- stl 容器(
std::vector,std::map等) - 智能指针、异常、枚举等
对复杂 c++ api 的封装能力远强于 ctypes。
结语
经过这两次学习,让小h更加能够体会到在代码的世界中,语言并不是孤立的,弱化了对语言的重视程度,学习编程更应该将编程语言看作一种工具,什么时候什么情况该换就换,大家各自完成各自擅长的部分,更有利于找到性能与效率的均衡点。
到此这篇关于python使用ctypes实现与c++互相调用的实践教程的文章就介绍到这了,更多相关python与c++互调内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论