- C++ 65.4%
- Shell 19.6%
- Python 8.7%
- CMake 6.3%
## [1.2.0-pre.1](https://git.aquila-consortium.org/guilhem_lavaux/generic_array/compare/v1.1.0...v1.2.0-pre.1) (2026-06-27) ### Features * add doc ([ |
||
|---|---|---|
| .forgejo/workflows | ||
| build_tools | ||
| docs | ||
| examples | ||
| include/generic_array | ||
| python | ||
| tests | ||
| .clang-format | ||
| .releaserc.json | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| CMakeLists.txt | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| VERSION.txt | ||
GenericArray
A lightweight, dynamic multi-dimensional array / view for C++20. Pure
container: shape, strides, memory ownership and zero-copy views, nothing
else. Optional, opt-in interop with xtensor (host math) and Kokkos
(device buffers).
#include <generic_array/generic_array.hpp>
using namespace ga;
Building
cmake -B build
cmake --build build
ctest --test-dir build
The core is header-only with zero dependencies. xtensor/Kokkos interop
and their examples/tests are auto-detected and built only if found.
1. Creating arrays
Two flavors: static rank (GenericArray<T, N>, rank known at compile
time, tightest codegen) and dynamic rank (GenericArray<T>, rank known
at runtime). Both share the same API.
// Owning allocation, static rank 2
auto a = GenericArray<double, 2>::from_shape({3, 4});
// Owning allocation, dynamic rank
auto b = GenericArray<int>::from_shape(std::vector<size_type>{2, 3, 5});
2. Element access
a(1, 2) = 3.14;
double v = a(1, 2);
operator() only works on host-accessible memory (it asserts otherwise —
device buffers are accessed via the Kokkos interop, not directly).
3. Wrapping external memory (no allocation)
std::vector<float> buf(6);
auto w = GenericArray<float, 2>::wrap(buf.data(), {2, 3});
w(0, 0) = 1.0f; // writes into buf
wrap never owns the memory — the caller keeps buf alive. For strided
external buffers, use wrap_strided(ptr, shape, strides, space, offset).
If you need the GenericArray to keep the real owner alive (e.g. a
Kokkos::View), use adopt(ptr, shape, strides, space, releaser) — see the
Kokkos section.
4. Views: transpose, slice, reshape
Views share the underlying buffer (refcounted) — no copies, and the buffer stays alive as long as any view of it exists.
auto a = GenericArray<int, 2>::from_shape({2, 3});
// Transpose -> shape (3,2), same memory
auto t = a.transpose(std::array<size_type,2>{1, 0});
// or, reverse all axes:
auto t2 = a.transpose();
// Slice -> rank-preserving sub-range (begin, end, step) per axis
auto s = a.slice(std::array<Range,2>{ Range{0, 2, 1}, Range{1, 3, 1} });
s(0, 0) = 42; // writes through to `a`
// Reshape (requires contiguous data)
auto r = a.reshape(std::array<size_type,2>{1, 6});
Dynamic-rank arrays additionally support squeeze() (drop all size-1 axes)
and reshape to a different rank:
auto d = GenericArray<int>::from_shape(std::vector<size_type>{1, 6, 1});
auto sq = d.squeeze(); // rank 1, shape (6)
auto flat = d.reshape(std::vector<size_type>{6}); // rank 1
5. Static <-> dynamic rank conversion
Both directions are O(1) and share storage (no copy):
auto dyn = a.to_dynamic(); // GenericArray<int, dynamic_rank>
auto back = dyn.to_static<2>(); // throws if dyn.rank() != 2
// non-throwing variant
if (auto opt = dyn.try_to_static<2>())
use(*opt);
6. Inspecting an array
a.rank(); // number of dimensions
a.shape(); // span<const size_type>
a.strides(); // span<const ssize_type>, in elements
a.size(); // total element count
a.space(); // MemorySpace::Host / Device / Managed
a.is_contiguous(); // row- or col-major contiguous
a.owns_data(); // false for wrap(), true for from_shape()/adopt()
a.data(); // T* to element (0,0,...,0)
7. xtensor interop (host, optional)
#include <generic_array/interop/xtensor.hpp>
auto a = GenericArray<double, 2>::from_shape({2, 3});
// Zero-copy: x aliases a's memory
auto x = ga::xt_interop::as_xtensor(a);
x(0, 0) = 1.0; // visible in a(0,0)
// Evaluate an xtensor expression into a new, owned GenericArray
auto y = ga::xt_interop::from_xtensor(x * 2.0 + 1.0);
8. Kokkos interop (host/device, optional)
#include <generic_array/interop/kokkos.hpp>
Kokkos::View<double**, Kokkos::HostSpace> v("v", 4, 5);
// Wrap: GenericArray aliases v's memory; v's refcount is captured and
// kept alive for as long as the GenericArray (or any view of it) lives.
auto a = ga::kokkos_interop::wrap(v); // GenericArray<double, 2>
// Hand a region back to Kokkos as an unmanaged View for a kernel
auto sub = a.slice(std::array<Range,2>{Range{1,3,1}, Range{0,5,1}});
auto kview = ga::kokkos_interop::as_kokkos_view<Kokkos::HostSpace>(sub);
If Kokkos was built with CUDA, compile with a CUDA-capable compiler (e.g.
Kokkos's nvcc_wrapper):
cmake -B build -DCMAKE_CXX_COMPILER=<kokkos_install>/bin/nvcc_wrapper
9. Python interop (optional)
GenericArray provides Python bindings via nanobind with support for:
- Python Buffer Protocol for zero-copy NumPy integration
- DLPack for cross-framework tensor exchange (PyTorch, JAX, etc.)
Building with Python support
GenericArray can automatically fetch nanobind using CMake's FetchContent:
# Enable Python bindings with automatic nanobind download
cmake -B build -DGA_ENABLE_PYTHON=ON \
-DGA_NANOBIND_FETCH=ON \
-DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build
# Optionally install the Python module
cmake --install build --component python
Alternatively, if you already have nanobind installed:
# With nanobind from pip
pip install nanobind
cmake -B build -DGA_ENABLE_PYTHON=ON \
-DGA_NANOBIND_FETCH=OFF \
-DGA_NANOBIND_ROOT=$(python3 -c "import nanobind; print(nanobind.get_include())")
cmake --build build
# Or with nanobind in a custom location
cmake -B build -DGA_ENABLE_PYTHON=ON \
-DGA_NANOBIND_FETCH=OFF \
-DGA_NANOBIND_ROOT=/path/to/nanobind/include
cmake --build build
Note: The first method (FetchContent) is recommended as it automatically downloads and uses a compatible version of nanobind.
Python usage
import generic_array as ga
import numpy as np
# Create a 3x4 float32 array
arr = ga.GenericArray_float32([3, 4])
# Element access
arr[0, 0] = 1.0
print(arr[1, 2]) # 0.0
# NumPy interop (zero-copy via Buffer Protocol)
np_arr = np.asarray(arr) # Zero-copy for contiguous arrays
np_arr[0, 0] = 2.0
print(arr[0, 0]) # 2.0 (mutual mutation)
# Convert to/from NumPy (both zero-copy; mutations are mutual)
np_view = ga.to_numpy(arr) # Zero-copy view; arr keeps the buffer alive
arr_view = ga.from_numpy(np_view) # Zero-copy view; arr_view keeps np_view alive
# DLPack interop
import torch # if available
dl_tensor = ga.to_dlpack(arr) # Export to DLPack
arr_back = ga.from_dlpack(dl_tensor) # Import from DLPack
torch_tensor = torch.from_dlpack(dl_tensor) # Import to PyTorch
# View operations
arr_t = arr.transpose() # Shape: [4, 3]
arr_slice = arr.slice([ga.Range(0, 2), ga.Range(1, 3)])
arr_flat = arr.reshape([1, 12])
arr_squeezed = ga.GenericArray_float32([1, 5, 1]).squeeze() # Shape: [5]
Supported types
GenericArray_float32/float64/float32_2D/float32_3D/ etc.GenericArray_int32/int64/int8GenericArray_uint8/uint32/uint64GenericArray_bool
Both dynamic-rank and static-rank (1D-4D) variants are available.
Python Buffer Protocol
GenericArray implements the Python Buffer Protocol (PEP 3118), enabling:
- Zero-copy conversion to NumPy arrays via
np.asarray() - Mutual mutations between GenericArray and NumPy
- Compatibility with any Python library that supports the buffer protocol
DLPack Support
DLPack enables zero-copy tensor exchange between frameworks:
- GenericArray ↔ NumPy
- GenericArray ↔ PyTorch (if available)
- GenericArray ↔ JAX (if available)
- GenericArray ↔ any DLPack-compatible framework
Python API Reference
See the Python API documentation for complete type hints.
What this class is not
No arithmetic, no broadcasting, no iterators, no I/O, no automatic host/device mirroring. It's a metadata + buffer-lifetime layer — build numeric operations on top via the xtensor/Kokkos/Python adapters.