From 2ccf1d0f99ef4a2a07368f70da6b032eeb1ae2c6 Mon Sep 17 00:00:00 2001 From: Mattia Penati Date: Sat, 14 Feb 2026 17:28:41 +0100 Subject: [PATCH 1/3] Build configuration to select the target API version --- Cargo.toml | 35 ++++++++++- build.rs | 1 + numpy-build-config/Cargo.toml | 33 +++++++++++ numpy-build-config/LICENSE | 25 ++++++++ numpy-build-config/src/impl_.rs | 101 ++++++++++++++++++++++++++++++++ numpy-build-config/src/lib.rs | 23 ++++++++ 6 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 numpy-build-config/Cargo.toml create mode 100644 numpy-build-config/LICENSE create mode 100644 numpy-build-config/src/impl_.rs create mode 100644 numpy-build-config/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 111834ca9..c5470983d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,8 @@ authors = [ ] description = "PyO3-based Rust bindings of the NumPy C-API" documentation = "https://docs.rs/numpy" -edition = "2021" -rust-version = "1.83" +edition.workspace = true +rust-version.workspace = true repository = "https://github.com/PyO3/rust-numpy" categories = ["api-bindings", "development-tools::ffi", "science"] keywords = ["python", "numpy", "ffi", "pyo3"] @@ -20,6 +20,29 @@ exclude = [ "x.py", ] +[features] +default = ["target-npy119"] + +# Default and minimum supported version are chosen to match the content of +# header `numpy/_core/include/numpy/numpyconfig.h`` in the first available +# version of numpy v2. +target-npy115 = ["numpy-build-config/target-npy115"] +target-npy116 = ["numpy-build-config/target-npy116"] +target-npy117 = ["numpy-build-config/target-npy117"] +target-npy118 = ["numpy-build-config/target-npy118"] +target-npy119 = ["numpy-build-config/target-npy119"] +target-npy120 = ["numpy-build-config/target-npy120"] +target-npy121 = ["numpy-build-config/target-npy121"] +target-npy122 = ["numpy-build-config/target-npy122"] +target-npy123 = ["numpy-build-config/target-npy123"] +target-npy124 = ["numpy-build-config/target-npy124"] +target-npy125 = ["numpy-build-config/target-npy125"] +target-npy20 = ["numpy-build-config/target-npy20"] +target-npy21 = ["numpy-build-config/target-npy21"] +target-npy22 = ["numpy-build-config/target-npy22"] +target-npy23 = ["numpy-build-config/target-npy23"] +target-npy24 = ["numpy-build-config/target-npy24"] + [dependencies] half = { version = "2.0", default-features = false, optional = true } libc = "0.2" @@ -37,6 +60,7 @@ nalgebra = { version = ">=0.30, <0.35", default-features = false, features = ["s [build-dependencies] pyo3-build-config = { version = "0.28", features = ["resolve-config"]} +numpy-build-config = { path = "numpy-build-config", version = "0.28.0" } [package.metadata.docs.rs] all-features = true @@ -47,3 +71,10 @@ elided-lifetimes-in-paths = "deny" [lints.clippy] needless-lifetimes = "allow" + +[workspace.package] +edition = "2021" +rust-version = "1.83" + +[workspace] +members = ["numpy-build-config"] diff --git a/build.rs b/build.rs index 0475124bb..2b1f14ad5 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,4 @@ fn main() { pyo3_build_config::use_pyo3_cfgs(); + numpy_build_config::use_numpy_cfgs(); } diff --git a/numpy-build-config/Cargo.toml b/numpy-build-config/Cargo.toml new file mode 100644 index 000000000..c6ae38b61 --- /dev/null +++ b/numpy-build-config/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "numpy-build-config" +version = "0.28.0" +authors = [ + "The rust-numpy Project Developers", + "PyO3 Project and Contributors ", +] +description = "Build configuration for the numpy crate" +edition = "2021" +rust-version = "1.83" +repository = "https://github.com/PyO3/rust-numpy" +license = "BSD-2-Clause" + +[dependencies] + +[features] +default = [] +target-npy115 = [] +target-npy116 = ["target-npy115"] +target-npy117 = ["target-npy116"] +target-npy118 = ["target-npy117"] +target-npy119 = ["target-npy118"] +target-npy120 = ["target-npy119"] +target-npy121 = ["target-npy120"] +target-npy122 = ["target-npy121"] +target-npy123 = ["target-npy122"] +target-npy124 = ["target-npy123"] +target-npy125 = ["target-npy124"] +target-npy20 = ["target-npy125"] +target-npy21 = ["target-npy20"] +target-npy22 = ["target-npy21"] +target-npy23 = ["target-npy22"] +target-npy24 = ["target-npy23"] diff --git a/numpy-build-config/LICENSE b/numpy-build-config/LICENSE new file mode 100644 index 000000000..d5b45151c --- /dev/null +++ b/numpy-build-config/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2017, Toshiki Teramura +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/numpy-build-config/src/impl_.rs b/numpy-build-config/src/impl_.rs new file mode 100644 index 000000000..d300b1a41 --- /dev/null +++ b/numpy-build-config/src/impl_.rs @@ -0,0 +1,101 @@ +#[derive(Debug, Clone, Copy)] +pub struct NumpyVersion { + pub minor: u32, + pub major: u32, +} + +#[allow(non_snake_case)] +impl NumpyVersion { + const fn V1(minor: u32) -> Self { + Self { major: 1, minor } + } + const fn V2(minor: u32) -> Self { + Self { major: 2, minor } + } +} + +impl NumpyVersion { + /// An iterator over supported versions of numpy API. + pub fn supported() -> impl Iterator { + SUPPORTED_VERSIONS.iter().copied() + } + + /// An iterator over enabled versions of numpy API. + pub fn enabled() -> impl Iterator { + ENABLED_VERSIONS.iter().copied() + } +} + +const SUPPORTED_VERSIONS: &[NumpyVersion] = &[ + NumpyVersion::V1(15), + NumpyVersion::V1(16), + NumpyVersion::V1(17), + NumpyVersion::V1(18), + NumpyVersion::V1(19), + NumpyVersion::V1(20), + NumpyVersion::V1(21), + NumpyVersion::V1(22), + NumpyVersion::V1(23), + NumpyVersion::V1(24), + NumpyVersion::V1(25), + NumpyVersion::V2(0), + NumpyVersion::V2(1), + NumpyVersion::V2(2), + NumpyVersion::V2(3), + NumpyVersion::V2(4), +]; + +const ENABLED_VERSIONS: &[NumpyVersion] = &[ + #[cfg(feature = "target-npy115")] + NumpyVersion::V1(15), // 0x0000000c + #[cfg(any( + feature = "target-npy116", + feature = "target-npy117", + feature = "target-npy118", + feature = "target-npy119" + ))] + NumpyVersion::V1(16), // 0x0000000d + #[cfg(any( + feature = "target-npy116", + feature = "target-npy117", + feature = "target-npy118", + feature = "target-npy119" + ))] + NumpyVersion::V1(17), // 0x0000000d + #[cfg(any( + feature = "target-npy116", + feature = "target-npy117", + feature = "target-npy118", + feature = "target-npy119" + ))] + NumpyVersion::V1(18), // 0x0000000d + #[cfg(any( + feature = "target-npy116", + feature = "target-npy117", + feature = "target-npy118", + feature = "target-npy119" + ))] + NumpyVersion::V1(19), // 0x0000000d + #[cfg(any(feature = "target-npy120", feature = "target-npy121"))] + NumpyVersion::V1(20), // 0x0000000e + #[cfg(any(feature = "target-npy120", feature = "target-npy121"))] + NumpyVersion::V1(21), // 0x0000000e + #[cfg(feature = "target-npy122")] + NumpyVersion::V1(22), // 0x0000000f + #[cfg(any(feature = "target-npy123", feature = "target-npy124"))] + NumpyVersion::V1(23), // 0x00000010 + #[cfg(any(feature = "target-npy123", feature = "target-npy124"))] + NumpyVersion::V1(24), // 0x00000010 + #[cfg(feature = "target-npy125")] + NumpyVersion::V1(25), // 0x00000011 + #[cfg(feature = "target-npy20")] + NumpyVersion::V2(0), // 0x00000012 + #[cfg(any(feature = "target-npy21", feature = "target-npy22"))] + NumpyVersion::V2(1), // 0x00000013 + #[cfg(any(feature = "target-npy21", feature = "target-npy22"))] + NumpyVersion::V2(2), // 0x00000013 + #[cfg(feature = "target-npy23")] + NumpyVersion::V2(3), // 0x00000014 + #[cfg(feature = "target-npy24")] + NumpyVersion::V2(4), // 0x00000015 +]; diff --git a/numpy-build-config/src/lib.rs b/numpy-build-config/src/lib.rs new file mode 100644 index 000000000..b9a9a2ff9 --- /dev/null +++ b/numpy-build-config/src/lib.rs @@ -0,0 +1,23 @@ +use self::impl_::NumpyVersion; + +mod impl_; + +pub fn use_numpy_cfgs() { + print_expected_features(); + print_enabled_features(); +} + +fn print_expected_features() { + for version in NumpyVersion::supported() { + println!( + "cargo:rustc-check-cfg=cfg(Numpy_{}_{})", + version.major, version.minor + ); + } +} + +fn print_enabled_features() { + for version in NumpyVersion::enabled() { + println!("cargo:rustc-cfg=Numpy_{}_{}", version.major, version.minor); + } +} From 0a5dc24b3a0665e60c67fc20481189342a5ad001 Mon Sep 17 00:00:00 2001 From: Mattia Penati Date: Sat, 14 Feb 2026 18:57:21 +0100 Subject: [PATCH 2/3] Fix struct definitions to match target api version --- src/npyffi/objects.rs | 49 ++++++++++++++++++++++++++++++++++++++++--- src/npyffi/types.rs | 6 ++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/npyffi/objects.rs b/src/npyffi/objects.rs index 4f4f1aaaf..b760fb64f 100644 --- a/src/npyffi/objects.rs +++ b/src/npyffi/objects.rs @@ -21,6 +21,12 @@ pub struct PyArrayObject { pub descr: *mut PyArray_Descr, pub flags: c_int, pub weakreflist: *mut PyObject, + + #[cfg(Numpy_1_20)] + pub _buffer_info: *mut c_void, + + #[cfg(Numpy_1_22)] + pub mem_handler: *mut PyObject, } #[repr(C)] @@ -32,6 +38,19 @@ pub struct PyArray_Descr { pub byteorder: c_char, pub _former_flags: c_char, pub type_num: c_int, + + #[cfg(Numpy_2_0)] + pub flags: npy_uint64, + #[cfg(Numpy_2_0)] + pub elsize: npy_intp, + #[cfg(Numpy_2_0)] + pub alignment: npy_intp, + #[cfg(Numpy_2_0)] + pub metadata: *mut PyObject, + #[cfg(Numpy_2_0)] + pub hash: npy_hash_t, + #[cfg(Numpy_2_0)] + pub reserved_null: [*mut c_void; 2], } #[repr(C)] @@ -365,11 +384,31 @@ pub struct PyUFuncObject { pub core_offsets: *mut c_int, pub core_signature: *mut c_char, pub type_resolver: PyUFunc_TypeResolutionFunc, - pub legacy_inner_loop_selector: PyUFunc_LegacyInnerLoopSelectionFunc, - pub reserved2: *mut c_void, - pub masked_inner_loop_selector: PyUFunc_MaskedInnerLoopSelectionFunc, + pub dict: *mut PyObject, + + #[cfg(all(Py_3_8, not(Py_LIMITED_API)))] + pub vectorcall: Option, + #[cfg(not(all(Py_3_8, not(Py_LIMITED_API))))] + pub vectorcall: *mut c_void, + + pub reserved3: *mut c_void, pub op_flags: *mut npy_uint32, pub iter_flags: npy_uint32, + + #[cfg(Numpy_1_16)] + pub core_dim_sizes: *mut npy_intp, + #[cfg(Numpy_1_16)] + pub core_dim_flags: *mut npy_uint32, + #[cfg(Numpy_1_16)] + pub identity_value: *mut PyObject, + + #[cfg(Numpy_1_22)] + pub _dispatch_cache: *mut c_void, + #[cfg(Numpy_1_22)] + pub _loops: *mut PyObject, + + #[cfg(Numpy_2_1)] + pub process_core_dims_func: PyUFunc_ProcessCoreDimsFunc, } pub type PyUFuncGenericFunction = @@ -415,6 +454,10 @@ pub type PyUFunc_MaskedInnerLoopSelectionFunc = Option< ) -> c_int, >; +#[cfg(Numpy_2_1)] +pub type PyUFunc_ProcessCoreDimsFunc = + Option c_int>; + #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct NpyIter([u8; 0]); diff --git a/src/npyffi/types.rs b/src/npyffi/types.rs index eb03b801b..e88a00552 100644 --- a/src/npyffi/types.rs +++ b/src/npyffi/types.rs @@ -154,6 +154,9 @@ pub enum NPY_SELECTKIND { NPY_INTROSELECT = 0, } +#[cfg(Numpy_2_4)] +pub const NPY_SAME_VALUE_CASTING_FLAG: u32 = 64; + #[repr(u32)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum NPY_CASTING { @@ -162,6 +165,9 @@ pub enum NPY_CASTING { NPY_SAFE_CASTING = 2, NPY_SAME_KIND_CASTING = 3, NPY_UNSAFE_CASTING = 4, + + #[cfg(Numpy_2_4)] + NPY_SAME_VALUE_CASTING = Self::NPY_UNSAFE_CASTING as u32 | NPY_SAME_VALUE_CASTING_FLAG, } #[repr(u32)] From 30dc6e9f36cccdb4c655cd9e8c0d4d32488069fc Mon Sep 17 00:00:00 2001 From: Mattia Penati Date: Tue, 24 Feb 2026 22:38:31 +0100 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f9500f38..d3c9a6c67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog - Unreleased + - Adjust struct definitions to match the selected NumPy C API version(#534) + - Add features to select the NumPy C API version (#534) - Fix PyArray_DTypeMeta definition when Py_LIMITED_API is disabled (#532) - v0.28.0