尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

nanobind:新一代C++/Python绑定工具,编译更快、性能更强

nanobind:新一代C++/Python绑定工具,编译更快、性能更强 1. 项目概述为什么我们需要一个新的C/Python绑定工具如果你是一名长期在C和Python之间“反复横跳”的开发者那么对“绑定”这个词一定又爱又恨。爱的是它能让我们把C那令人安心的性能和Python那无与伦比的灵活性结合起来创造出既快又好用的工具。恨的是这个过程本身往往充满了“坑”漫长的编译时间、晦涩难懂的宏、复杂的内存管理还有那动不动就出现的“Segmentation fault”。在很长一段时间里pybind11几乎是这个领域的唯一选择它很强大但有时也显得过于“厚重”。直到我遇到了nanobind它给我的感觉就像是在一个闷热的房间里突然打开了一扇窗。nanobind是什么简单说它是一个用于创建C/Python绑定的现代库。它的目标非常明确更快、更小、更简单。这里的“快”是双重的既指它生成的绑定代码执行速度快也指它本身的编译速度快得惊人。我第一次用它替换一个中等规模的pybind11项目时编译时间从近一分钟缩短到了十几秒这种体验上的提升是颠覆性的。它并非要完全取代pybind11而是为那些对性能、编译速度和二进制包大小有极致要求的场景提供了一个锋利的新选择。无论是开发高性能的科学计算库、游戏引擎的脚本接口还是需要频繁迭代的插件系统nanobind都值得你深入了解。2. nanobind核心设计哲学与优势解析2.1 与pybind11的核心理念差异要理解nanobind最好从它与pybind11的对比开始。两者师出同门nanobind的作者也是pybind11的核心贡献者之一。但它们的侧重点截然不同。pybind11的设计哲学是“功能完备”。它致力于覆盖CPython C API的方方面面提供极其丰富的特性从基本的函数、类绑定到复杂的STL容器自动转换、智能指针管理、自定义异常甚至对NumPy数组的深度支持。这种完备性带来了强大的能力但也引入了复杂性。它的头文件很大编译时需要解析大量的模板代码导致编译速度较慢。生成的二进制文件也相对较大因为它包含了许多你可能用不到的“兜底”逻辑。而nanobind的设计哲学则是“精准高效”。它像一个外科手术刀只提供最核心、最常用的绑定功能并且对每一行代码都进行了极致的性能优化。它默认使用C17标准大量利用现代C的特性如constexpr、模板元编程在编译期完成更多工作从而减少运行时的开销。它的一个核心目标是生成更小的二进制文件这对于需要分发的Python包尤其是通过PyPI至关重要能显著减少用户的下载和安装时间。2.2 性能优势的具体体现nanobind的性能优势不是空谈它体现在几个可量化的方面编译时间Compile Time这是最直观的体验。由于代码库更精简模板实例化更少在相同项目上使用nanobind的编译时间通常只有pybind11的1/3 到 1/5。在持续集成CI和日常开发中这节省的时间是巨大的。二进制大小Binary Size生成的.so或.pyd文件更小。一个简单的绑定模块大小减少30%-50%很常见。这得益于更精简的类型系统实现和更少的静态初始化代码。运行时性能Runtime Performance函数调用、对象构造、属性访问等基础操作的开销更低。nanobind在内部数据结构和对齐上做了大量优化减少了间接寻址和缓存未命中。对于高频调用的绑定函数性能提升可能达到10%-20%。内存占用Memory Footprint绑定的类型信息、函数表等内部数据结构占用内存更少。注意nanobind并非在所有方面都超越pybind11。例如它对某些非常边缘的CPython API支持可能不如pybind11全面。但对于95%的常见绑定需求nanobind在提供同等功能的前提下性能表现都更优。2.3 适用场景与决策指南那么什么时候该选择nanobind而不是pybind11呢我根据自己的经验总结了一个简单的决策树如果你的项目是全新的且对性能、编译速度或包大小有要求毫不犹豫地从nanobind开始。如果你在维护一个大型的、稳定的pybind11项目且没有遇到性能瓶颈可以继续使用pybind11迁移需要成本。如果你在开发一个需要分发给广大Python用户的库尤其是通过pip installnanobind更小的二进制尺寸是一个显著优势。如果你的绑定代码非常复杂用到了pybind11的一些高级或实验性特性需要仔细评估nanobind是否支持或者是否有替代方案。如果你受够了漫长的编译等待希望提升开发效率nanobind是绝佳的解药。3. 从零开始构建你的第一个nanobind项目3.1 环境准备与依赖安装开始之前你需要确保系统环境就绪。nanobind的核心依赖很简单一个支持C17的编译器GCC 7、Clang 5 或 MSVC 2019 都可以。我个人推荐使用较新的版本以获得更好的编译体验。CMake 3.16这是目前最主流的构建系统配置方式。Python 3.8及开发头文件在Ubuntu/Debian上通常需要安装python3-dev包在macOS上使用Homebrew安装的Python通常自带在Windows上如果你使用官方安装器请确保勾选了“安装开发工具”或类似选项。nanobind本身是一个头文件库Header-only但为了管理方便我们通常通过CMake的FetchContent来获取它。这是我最推荐的方式因为它能自动处理版本和依赖与你的项目构建流程无缝集成。3.2 最小化CMake项目配置让我们从一个最干净的项目结构开始。假设你的项目目录如下my_nanobind_project/ ├── CMakeLists.txt ├── src/ │ └── mymodule.cpp └── pyproject.toml (可选用于打包)核心的CMakeLists.txt文件可以这样编写cmake_minimum_required(VERSION 3.16) project(MyNanobindProject LANGUAGES CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 查找Python解释器和开发库 find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module) # 使用FetchContent引入nanobind include(FetchContent) FetchContent_Declare( nanobind GIT_REPOSITORY https://github.com/wjakob/nanobind.git GIT_TAG v2.0.0 # 建议指定一个稳定版本标签 ) FetchContent_MakeAvailable(nanobind) # 添加你的模块 add_library(mymodule MODULE src/mymodule.cpp) # 将扩展模块后缀设置为Python可识别的格式.so, .pyd等 set_target_properties(mymodule PROPERTIES PREFIX SUFFIX ${PYTHON_MODULE_EXTENSION}) # 关键链接nanobind和Python库 target_link_libraries(mymodule PRIVATE nanobind Python::Python) # 包含nanobind头文件目录 target_include_directories(mymodule PRIVATE ${nanobind_SOURCE_DIR}/include) # 可选将编译好的模块复制到项目根目录方便测试 add_custom_command(TARGET mymodule POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $TARGET_FILE:mymodule ${CMAKE_BINARY_DIR} )这个配置做了几件关键事强制使用C17。找到系统Python。动态下载并引入指定版本的nanobind。将你的C代码编译成一个动态库模块并正确设置名称。将nanobind和 Python 库链接到你的模块。3.3 编写第一个绑定函数与类现在我们来编写src/mymodule.cpp实现一个简单的数学工具模块#include nanobind/nanobind.h #include nanobind/stl/string.h // 如果需要绑定std::string #include nanobind/stl/vector.h // 如果需要绑定std::vector #include cmath namespace nb nanobind; // 使用简短的命名空间别名 // 1. 绑定一个简单的函数 int add(int a, int b) { return a b; } // 2. 绑定一个带有默认参数和文档字符串的函数 double compute_radius(double area, const std::string shape circle) { if (shape circle) { return std::sqrt(area / M_PI); } else { // 抛出一个Python异常 nb::raise(ValueError, Unsupported shape); return 0.0; // 这行不会执行仅为编译需要 } } // 3. 定义一个要绑定的C类 class Person { public: Person(const std::string name, int age) : name_(name), age_(age) {} void greet() const { printf(Hello, my name is %s and Im %d years old.\n, name_.c_str(), age_); } void have_birthday() { age_; } // Getter 和 Setter std::string get_name() const { return name_; } void set_name(const std::string name) { name_ name; } int get_age() const { return age_; } // 注意我们没有提供 set_age意味着年龄在Python端是只读属性除了通过have_birthday修改 private: std::string name_; int age_; }; // 4. 使用NB_MODULE宏定义模块入口点 NB_MODULE(mymodule, m) { // 设置模块的文档字符串 m.doc() A simple example module built with nanobind; // 绑定函数 m.def(add, add, nb::arg(a), nb::arg(b), Add two integers); m.def(compute_radius, compute_radius, nb::arg(area), nb::arg(shape) circle, Compute radius from area for a given shape); // 绑定类 nb::class_Person(m, Person) .def(nb::initconst std::string, int(), nb::arg(name), nb::arg(age)) .def(greet, Person::greet) .def(have_birthday, Person::have_birthday) .def_prop_rw(name, Person::get_name, Person::set_name) // 读写属性 .def_prop_ro(age, Person::get_age) // 只读属性 .def(__repr__, [](const Person p) { return Person name p.get_name() age std::to_string(p.get_age()) ; }); }实操心得注意NB_MODULE宏的第一个参数mymodule必须与你在CMakeLists.txt中add_library指定的目标名以及最终生成的动态库文件名不含后缀完全一致否则Python在导入时会找不到模块。这是新手最容易踩的坑。3.4 编译与测试在项目根目录下执行标准的CMake构建流程mkdir build cd build cmake .. cmake --build . --config Release在Linux/macOS上你会在build目录下找到mymodule.so在Windows上则是mymodule.pyd。现在启动Python解释器测试import sys sys.path.insert(0, ‘/path/to/your/project/build‘) # 添加构建目录到Python路径 import mymodule print(mymodule.add(5, 3)) # 输出: 8 radius mymodule.compute_radius(78.5) print(f“Radius: {radius}“) # 输出: Radius: 5.0 try: mymodule.compute_radius(100, “square“) except ValueError as e: print(e) # 输出: Unsupported shape p mymodule.Person(“Alice“, 30) p.greet() # 输出: Hello, my name is Alice and I‘m 30 years old. print(p.name, p.age) # 输出: Alice 30 p.name “Alicia“ p.have_birthday() print(p) # 输出: Person name‘Alicia‘ age31 # p.age 32 # 这行会报错因为age是只读属性恭喜你已经成功创建了第一个nanobind模块。整个过程比想象中更简洁不是吗4. 深入核心高级绑定特性与内存管理4.1 类型转换与STL容器支持nanobind内置了对许多C标准库类型和Python类型之间自动转换的支持这极大地简化了绑定工作。你只需要包含对应的头文件即可。#include nanobind/stl/string.h #include nanobind/stl/vector.h #include nanobind/stl/map.h #include nanobind/stl/pair.h #include nanobind/stl/optional.h #include nanobind/eigen.h // 如果需要Eigen矩阵支持 // 自动转换示例 std::vectorint process_data(const std::vectorfloat input) { std::vectorint output; output.reserve(input.size()); for (auto f : input) { output.push_back(static_castint(f * 10)); } return output; // nanobind会自动将其转换为Python list } std::mapstd::string, int count_words(const std::string text) { std::mapstd::string, int counts; // ... 简单的分词和计数逻辑 counts[“hello“] 2; counts[“world“] 1; return counts; // 自动转换为Python dict } NB_MODULE(advanced_types, m) { m.def(“process_data“, process_data); m.def(“count_words“, count_words); }在Python中你可以直接传递list和dictimport advanced_types result advanced_types.process_data([1.1, 2.2, 3.3]) print(result) # 输出: [11, 22, 33] word_counts advanced_types.count_words(“hello world hello“) print(word_counts) # 输出: {‘hello‘: 2, ‘world‘: 1}注意事项自动转换虽然方便但对于非常大的数据容器在C和Python之间来回拷贝可能会成为性能瓶颈。对于性能关键的数据交换考虑使用缓冲区协议Buffer Protocol或内存视图Memory Viewsnanobind对此也有很好的支持允许你在不复制数据的情况下在C中访问NumPy数组等对象。4.2 智能指针与对象生命周期管理在C/Python边界管理对象生命周期是一个核心挑战。nanobind提供了清晰的策略。独占所有权std::unique_ptr当C函数返回一个std::unique_ptr时nanobind会将其所有权转移给Python。当Python对象被垃圾回收时C对象也会被删除。这是最安全、最推荐的方式。#include memory class Resource { /* ... */ }; std::unique_ptrResource create_resource() { return std::make_uniqueResource(); }共享所有权std::shared_ptr当C和Python都需要持有对象的引用时使用。nanobind能很好地处理std::shared_ptrPython端的引用计数会与C的共享指针计数联动。你需要包含nanobind/stl/shared_ptr.h。#include nanobind/stl/shared_ptr.h class SharedObject { /* ... */ }; std::shared_ptrSharedObject g_global_object; void set_global(std::shared_ptrSharedObject obj) { g_global_object obj; } std::shared_ptrSharedObject get_global() { return g_global_object; }引用现有对象裸指针或引用这是最危险但也有时是必要的。你需要确保Python对象存活期间其引用的C对象不会被销毁。nanobind提供了nb::keep_alivecaret, life调用策略来声明这种依赖关系防止在C对象还被需要时被意外回收。class Node { public: Node* child nullptr; }; // 绑定一个设置子节点的函数需要保证在Node对象存活期间其child指向的对象也存活。 // 但更安全的设计是使用智能指针。最佳实践对于在堆上分配、并且所有权需要跨越语言边界传递的对象优先使用std::unique_ptr。仅在确需共享所有权时使用std::shared_ptr。尽量避免直接暴露裸指针。4.3 自定义异常与错误处理nanobind使得在C中抛出Python异常变得非常简单。你可以使用nb::raise(“ExceptionType“, “message“)或者为特定的C异常类型定义转换。#include stdexcept void risky_operation(int value) { if (value 0) { // 直接抛出Python异常 nb::raise(“ValueError“, “Input value must be non-negative“); } if (value 100) { // 也可以抛出C异常并让nanobind转换 throw std::out_of_range(“Value is too large“); } // 正常操作... } // 你可以注册C异常到Python异常的映射通常在模块初始化时做一次 NB_MODULE(error_handling, m) { // 注册 std::exception 及其子类到 Python 的 RuntimeError nb::register_exception_translator([](std::exception_ptr p) { try { if (p) std::rethrow_exception(p); } catch (const std::exception e) { nb::raise(“RuntimeError“, e.what()); } }); m.def(“risky_operation“, risky_operation); }在Python中这些异常会被正常捕获import error_handling try: error_handling.risky_operation(-5) except ValueError as e: print(f“Caught ValueError: {e}“) try: error_handling.risky_operation(200) except RuntimeError as e: # 注意std::out_of_range被转换成了RuntimeError print(f“Caught RuntimeError: {e}“)5. 性能调优与进阶技巧5.1 减少绑定开销避免不必要的拷贝绑定接口的性能瓶颈常常出现在数据拷贝上。nanobind提供了几种工具来避免或减少拷贝。使用nb::call_guard与nb::rv_policynb::rv_policy控制返回值的策略。例如nb::rv_policy::reference_internal表示返回的是一个内部引用其生命周期依赖于调用它的对象self。这可以避免返回一个大型对象如字符串时的拷贝。class BigDataHolder { std::string huge_string; public: const std::string get_data() const { return huge_string; } }; nb::class_BigDataHolder(m, “BigDataHolder“) .def(nb::init()) .def(“get_data“, BigDataHolder::get_data, nb::rv_policy::reference_internal);nb::call_guard可以用来在函数调用前后执行一些操作但更常见的是用它来“守护”一些资源确保在函数执行期间某些对象保持存活。它常与rv_policy结合使用。利用缓冲区协议进行零拷贝数据交换 这是与NumPy、Pillow等库进行高效交互的关键。nanobind的nb::buffer和nb::ndarray支持使得这变得容易。#include nanobind/ndarray.h void process_image(nb::ndarrayconst uint8_t, nb::ndim3 img) { // img是一个三维数组例如高度x宽度x通道数据是只读的 // 直接访问底层指针无需拷贝 const uint8_t* data img.data(); size_t height img.shape(0); size_t width img.shape(1); size_t channels img.shape(2); // ... 处理图像数据 } NB_MODULE(fast_image, m) { m.def(“process_image“, process_image); }在Python端你可以直接传递一个NumPy数组import numpy as np import fast_image img np.random.randint(0, 256, (480, 640, 3), dtypenp.uint8) fast_image.process_image(img) # 没有数据拷贝发生5.2 编译期优化与模板元编程nanobind大量使用C模板元编程在编译期生成最优的绑定代码。作为使用者你可以通过以下方式配合使用constexpr和noexcept尽可能将你的C函数标记为constexpr如果可能和noexcept。这不仅能给编译器更多优化提示有时也能帮助nanobind生成更高效的调用路径。避免在绑定代码中使用虚函数或RTTI除非必要。简单的、非虚拟的接口绑定效率最高。利用nb::type进行编译期类型查询在高级场景中你可能需要根据类型信息进行不同的操作。nanobind提供了编译期类型查询机制比运行时的typeid或字符串比较更高效。5.3 模块化与大型项目组织当绑定代码规模增长时良好的组织至关重要。拆分绑定代码不要把所有绑定都塞进一个巨大的NB_MODULE里。你可以将不同类的绑定分散到不同的.cpp文件中每个文件包含自己的nb::class_定义然后在主模块文件中用m.def_submodule或简单地通过链接多个目标来组合。使用CMake目标链接为每个逻辑组创建一个CMake目标库最后将它们链接到主模块目标中。这有利于增量编译和代码复用。add_library(core_bindings STATIC src/core_bindings.cpp) target_link_libraries(core_bindings PRIVATE nanobind) add_library(extra_bindings STATIC src/extra_bindings.cpp) target_link_libraries(extra_bindings PRIVATE nanobind) add_library(mymodule MODULE src/module_main.cpp) target_link_libraries(mymodule PRIVATE nanobind core_bindings extra_bindings Python::Python)注意初始化顺序如果拆分绑定要确保全局变量或静态变量的初始化顺序不会导致问题。nanobind的模块初始化是线程安全的但复杂的跨模块依赖仍需小心。6. 实战将一个小型C库完整绑定到Python让我们以一个假设的、简单的“几何计算库”为例演示一个更完整的绑定过程。这个库包含点Point、向量Vector和矩形Rectangle类以及一些工具函数。项目结构geometry_py/ ├── CMakeLists.txt ├── include/ │ ├── geometry/ │ │ ├── point.hpp │ │ ├── vector.hpp │ │ └── rectangle.hpp ├── src/ │ ├── geometry/ │ │ ├── point.cpp │ │ ├── vector.cpp │ │ └── rectangle.cpp │ └── bindings/ │ ├── bind_point.cpp │ ├── bind_vector.cpp │ ├── bind_rectangle.cpp │ └── module.cpp └── tests/ └── test_geometry.py关键绑定代码示例 (src/bindings/bind_point.cpp):#include nanobind/nanobind.h #include nanobind/operators.h // 启用运算符重载绑定 #include nanobind/stl/string.h #include “../../include/geometry/point.hpp“ namespace nb nanobind; using namespace geometry; NB_MODULE(_geometry_internal, m) { // 绑定 Point 类 nb::class_Point(m, “Point“) .def(nb::initdouble, double(), nb::arg(“x“)0.0, nb::arg(“y“)0.0) .def_rw(“x“, Point::x) .def_rw(“y“, Point::y) .def(“distance_to“, Point::distance_to) .def(“__repr__“, [](const Point p) { return “Point(“ std::to_string(p.x) “, “ std::to_string(p.y) “)“; }) // 重载运算符 .def(nb::self nb::self) // Point Point - Vector .def(nb::self nb::self) .def(nb::self ! nb::self); }主模块文件 (src/bindings/module.cpp):#include nanobind/nanobind.h // 注意这里不直接包含具体的绑定实现而是声明初始化函数 // 这些函数在各自的bind_*.cpp中定义 void init_point(nb::module_); void init_vector(nb::module_); void init_rectangle(nb::module_); NB_MODULE(geometry, m) { m.doc() “A high-performance geometry library for Python“; // 初始化各个子模块 init_point(m); init_vector(m); init_rectangle(m); // 绑定自由函数 m.def(“midpoint“, geometry::midpoint); m.def(“is_point_in_rect“, geometry::is_point_in_rect); }对应的CMakeLists.txt需要将所有这些绑定源文件链接到一起。Python测试 (tests/test_geometry.py):import sys sys.path.insert(0, ‘build‘) import geometry p1 geometry.Point(1, 2) p2 geometry.Point(4, 6) print(p1) # 输出: Point(1.000000, 2.000000) print(p1.distance_to(p2)) # 输出: 5.0 v p1 p2 # 调用重载的运算符 print(v) # 输出: Vector(5.0, 8.0) rect geometry.Rectangle(geometry.Point(0, 0), 10, 20) print(geometry.is_point_in_rect(p1, rect)) # 输出: True通过这个结构你的C库被清晰地暴露给了Python同时保持了C项目本身的模块化。7. 常见问题排查与调试技巧即使有了完善的工具绑定过程中也难免遇到问题。这里记录了一些我踩过的坑和解决方法。7.1 编译期问题问题现象可能原因解决方案undefined reference totypeinfo for ...‘绑定的C类缺少虚函数表vtable通常是因为类的定义中声明了虚函数但没有实现或者没有关键函数的实现。确保类的所有虚函数都有定义即使是纯虚函数在绑定前也需要一个实现哪怕是空的。检查.cpp文件是否被正确编译和链接。error: static assertion failed: ...模板实例化失败。常见于nb::class_绑定一个不完整的类型或者类型不满足nanobind的某些概念要求如可复制构造。确保在绑定 (nb::class_T) 时类型T的定义是完整的即编译器已经看到了class T { ... };的全部内容。对于仅前向声明的类型无法绑定。CMake找不到PythonPython开发包未安装或CMake的find_package使用了错误的路径。确认python3-dev(或python-devel) 已安装。可以尝试在CMake中指定Python根目录-DPython_ROOT_DIR/usr/local。编译时间依然很长单个绑定文件包含了太多模板实例化或者引入了大量沉重的头文件如windows.h,boost/...。使用前置声明Forward Declaration和Pimpl惯用法来减少头文件依赖。将绑定代码拆分成多个小的编译单元。7.2 运行时问题问题现象可能原因解决方案ImportError: dynamic module does not define module export functionNB_MODULE宏中的模块名与生成的动态库文件名不匹配或者函数签名错误。百分之百检查NB_MODULE(mymodule, m)中的mymodule是否与add_library(mymodule ...)中的目标名完全一致包括大小写。AttributeError: module ‘xxx‘ has no attribute ‘yyy‘绑定代码没有成功将函数或类暴露给模块。检查绑定代码m.def或nb::class_是否被正确执行。确保包含绑定代码的源文件被链接到了最终的模块中。Segmentation fault内存管理问题。例如Python对象持有了一个已被删除的C对象的裸指针或者在C回调中错误地创建/删除了Python对象。1.优先使用智能指针(unique_ptr,shared_ptr)。2. 使用nb::keep_alive调用策略明确生命周期依赖。3. 在C代码中操作Python对象时使用nb::gil_scoped_acquire和nb::gil_scoped_release来管理全局解释器锁GIL避免多线程问题。性能不如预期函数调用开销大或数据拷贝过多。1. 检查是否可以使用nb::rv_policy::reference_internal或nb::rv_policy::reference来避免返回值拷贝。2. 对于数值计算密集型函数确保使用nb::ndarray进行零拷贝数据传递。3. 使用性能分析工具如cProfile或py-spy定位热点。7.3 调试技巧使用调试符号编译在CMake中设置-DCMAKE_BUILD_TYPEDebug。这允许你在GDB或LLDB中设置断点查看C栈帧。在Python中触发崩溃后进入调试器在终端中运行python -m pdb your_script.py当段错误发生时操作系统可能会生成核心转储core dump你可以用gdb python core来加载分析。nanobind内部调试nanobind本身也提供了一些调试支持。例如你可以通过定义宏NANOBIND_DEBUG来开启一些内部检查但这通常只在开发nanobind本身时有用。隔离测试创建一个最小的、只重现问题的测试用例。这能帮你快速定位是绑定代码的问题还是底层C库的问题。8. 打包与分发制作专业的Python包开发完成后你需要将你的模块打包以便其他人可以通过pip install轻松安装。这涉及到为不同平台Windows, macOS, Linux编译二进制轮子wheel。8.1 使用scikit-build-core(现代推荐)目前最推荐的方式是使用scikit-build-core它是传统setuptools和scikit-build的现代替代品对CMake项目支持更好配置更简洁。创建pyproject.toml:[build-system] requires [“scikit-build-core0.5.0“, “cmake3.16“, “ninja“] build-backend “scikit_build_core.build“ [project] name “my-geometry-library“ version “0.1.0“ authors [{name “Your Name“, email “youexample.com“}] description “A high-performance geometry library with Python bindings“ readme “README.md“ license {text “MIT“} classifiers [ “Programming Language :: Python :: 3“, “Programming Language :: C“, “License :: OSI Approved :: MIT License“, “Operating System :: OS Independent“, ] dependencies [“numpy1.20“] # 如果你的库依赖NumPy dynamic [“dependencies“] # 可选如果你有动态依赖 [tool.scikit-build] # 指定CMake的最小版本 cmake.minimum-version “3.16“ # 构建目录通常不需要改 build-dir “build“ # 指定需要包含在wheel中的文件 wheel.packages [{from “build“, to “my_geometry_library“}]创建CMakeLists.txt: 这个CMakeLists.txt就是之前我们写的那个但需要做一些调整使其能感知到scikit-build-core传递的Python路径等信息。通常scikit-build-core会自动设置好Python_EXECUTABLE等变量。构建与安装:# 从源码安装开发模式 pip install -e . # 构建wheel pip install build python -m build执行后会在dist/目录下生成.whl文件。8.2 使用cibuildwheel进行跨平台构建为了给Windows、macOS和Linux等多个平台生成wheel你需要使用cibuildwheel。它通常在GitHub Actions、Azure Pipelines等CI环境中运行。一个简单的.github/workflows/build_wheels.yml示例name: Build wheels on: [push, pull_request] jobs: build_wheels: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-22.04, windows-latest, macos-latest] steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: ‘3.10‘ - name: Install cibuildwheel run: pip install cibuildwheel2.16.0 - name: Build wheels run: python -m cibuildwheel --output-dir wheelhouse env: CIBW_BUILD: “cp310-*“ # 为Python 3.10构建 CIBW_ARCHS_LINUX: auto64 CIBW_BEFORE_ALL_LINUX: “yum install -y python3-devel || apt-get install -y python3-dev || true“ # 安装开发头文件 - uses: actions/upload-artifactv3 with: name: wheels path: ./wheelhouse/*.whl8.3 上传到PyPI生成wheel后你可以使用twine上传到PyPI或TestPyPI。pip install twine # 上传到TestPyPI测试 twine upload --repository-url https://test.pypi.org/legacy/ dist/* # 上传到正式的PyPI twine upload dist/*至此你的高性能C/Python绑定库就可以被全世界的Python开发者通过一句简单的pip install my-geometry-library来使用了。从一行绑定代码到一个可分发的专业包nanobind提供了一条高效、清晰的路径。它削减了传统绑定中的繁文缛节让你能更专注于库本身的功能和性能。
返回列表