diff --git a/CMakeLists.txt b/CMakeLists.txt index 550972a..9457a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,10 +62,12 @@ endif() ## Python bindings option(BUILD_PYTHON "Build velodyne_decoder_pylib Python module" FALSE) if(BUILD_PYTHON OR SKBUILD) - find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) - find_package(pybind11 CONFIG REQUIRED) + find_package(Python + REQUIRED COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) + find_package(nanobind CONFIG REQUIRED) - pybind11_add_module(python_bindings src/python.cpp) + nanobind_add_module(python_bindings STABLE_ABI NB_STATIC src/python.cpp) set_target_properties(python_bindings PROPERTIES OUTPUT_NAME velodyne_decoder_pylib) target_include_directories(python_bindings PRIVATE include) target_link_libraries(python_bindings PRIVATE velodyne_decoder) diff --git a/pyproject.toml b/pyproject.toml index 18ddee6..9673dda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ include-package-data = true requires = [ "scikit-build-core >=0.4.3", "conan >=2.0.5", - "pybind11" + "nanobind" ] build-backend = "scikit_build_core.build" @@ -47,6 +47,8 @@ cmake.targets = ["python_bindings"] install.components = ["python"] wheel.license-files = ["LICENSE"] build-dir = "build/{wheel_tag}" +# Build stable ABI wheels for CPython 3.12+ +wheel.py-api = "cp312" [tool.scikit-build.cmake.define] BUILD_PYTHON = true diff --git a/src/python.cpp b/src/python.cpp index 96d3de5..d3f47c5 100644 --- a/src/python.cpp +++ b/src/python.cpp @@ -4,12 +4,16 @@ #include #include #include -#include +#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include "velodyne_decoder/config.h" #include "velodyne_decoder/scan_decoder.h" @@ -17,237 +21,260 @@ #include "velodyne_decoder/telemetry_packet.h" #include "velodyne_decoder/types.h" -namespace py = pybind11; -using namespace pybind11::literals; +namespace nb = nanobind; +using namespace nanobind::literals; using namespace velodyne_decoder; -/** - * Zero-copy conversion to py::array. - */ -template ::value>::type> -inline py::array_t as_pyarray(Sequence &&seq) { - auto *seq_ptr = new Sequence(seq); - auto capsule = py::capsule(seq_ptr, [](void *p) { delete reinterpret_cast(p); }); - return py::array(seq_ptr->size(), seq_ptr->data(), capsule); +constexpr int ncols = 8; +using NumpyPointCloud = nb::ndarray, nb::c_contig>; + +nb::dict get_point_dtype() { + nb::dict dtype_dict; + dtype_dict["names"] = + nb::make_tuple("x", "y", "z", "intensity", "time", "column", "ring", "return_type"); + dtype_dict["formats"] = nb::make_tuple("swap(cloud); + nb::capsule deleter(tmp, [](void *p) noexcept { delete static_cast(p); }); + nb::object arr = nb::cast(NumpyPointCloud{tmp->data(), {n, ncols}, deleter}); + arr.attr("dtype") = get_point_dtype(); + arr.attr("shape") = nb::make_tuple(n); + return arr; } -py::array as_contiguous(const PointCloud &cloud) { - const int ncols = 8; - std::vector> arr; - arr.reserve(cloud.size()); - for (const auto &p : cloud) { - arr.push_back( - {p.x, p.y, p.z, p.intensity, p.time, (float)p.column, (float)p.ring, (float)p.return_type}); +NumpyPointCloud as_contiguous(const PointCloud &cloud) { + const size_t n = cloud.size(); + float *arr = new float[n * ncols]; + nb::capsule deleter(arr, [](void *p) noexcept { delete static_cast(p); }); + NumpyPointCloud py_arr = {arr, {n, ncols}, deleter}; + for (size_t i = 0; i < n; i++) { + const auto &p = cloud[i]; + py_arr(i, 0) = p.x; + py_arr(i, 1) = p.y; + py_arr(i, 2) = p.z; + py_arr(i, 3) = p.intensity; + py_arr(i, 4) = p.time; + py_arr(i, 5) = static_cast(p.column); + py_arr(i, 6) = static_cast(p.ring); + py_arr(i, 7) = static_cast(p.return_type); } - return as_pyarray(std::move(arr)); + return py_arr; } -py::array convert(PointCloud &cloud, bool as_pcl_structs) { +nb::object convert(PointCloud &&cloud, bool as_pcl_structs) { if (as_pcl_structs) { - return as_pyarray(std::move(cloud)); - } else { - return as_contiguous(cloud); + return as_recarray(std::move(cloud)); } + return nb::cast(as_contiguous(cloud)); } -py::object to_datetime(Time time) { - static auto fromtimestamp = py::module::import("datetime").attr("datetime").attr("fromtimestamp"); +nb::object to_datetime(Time time) { + static auto fromtimestamp = + nb::module_::import_("datetime").attr("datetime").attr("fromtimestamp"); return fromtimestamp(time); } -py::object convert_gps_time(std::optional