scirs2_core/memory_efficient/views.rs
1use super::validation;
2use crate::error::{CoreError, ErrorContext, ErrorLocation};
3use ::ndarray::{
4 Array, ArrayBase, ArrayView as NdArrayView, ArrayViewMut as NdArrayViewMut, Data, Dimension,
5 Ix1, Ix2, RawDataMut,
6};
7
8/// A type alias for ndarray's ArrayView with additional functionality
9pub type ArrayView<'a, A, D> = NdArrayView<'a, A, D>;
10
11/// A type alias for ndarray's ArrayViewMut with additional functionality
12pub type ViewMut<'a, A, D> = NdArrayViewMut<'a, A, D>;
13
14/// Create a view of an array with a different element type
15///
16/// This function creates a view of the given array interpreting its elements
17/// as a different type. This is useful for viewing binary data as different
18/// types without copying.
19///
20/// # Safety
21///
22/// This function is unsafe because it does not check that the memory layout
23/// of the source type is compatible with the destination type.
24///
25/// # Arguments
26///
27/// * `array` - The array to view
28///
29/// # Returns
30///
31/// A view of the array with elements interpreted as the new type
32pub unsafe fn view_as<A, B, S, D>(array: &ArrayBase<S, D>) -> Result<ArrayView<'_, B, D>, CoreError>
33where
34 A: Clone,
35 S: Data<Elem = A>,
36 D: Dimension,
37{
38 validation::check_not_empty(array)?;
39
40 // Calculate new shape based on type sizes
41 let a_size = std::mem::size_of::<A>();
42 let b_size = std::mem::size_of::<B>();
43
44 if a_size == 0 || b_size == 0 {
45 return Err(CoreError::ValidationError(
46 ErrorContext::new("Cannot reinterpret view of zero-sized type".to_string())
47 .with_location(ErrorLocation::new(file!(), line!())),
48 ));
49 }
50
51 if a_size != b_size {
52 // Reinterpreting between differently-sized element types requires a shape change
53 // that cannot be expressed within the generic `D` dimension parameter; use a
54 // 1-D-specific helper or reshape the array to Ix1 before calling view_as.
55 return Err(CoreError::NotImplementedError(
56 ErrorContext::new(format!(
57 "view_as with differing element sizes ({a_size} vs {b_size}) requires a shape \
58 change not representable in the generic dimension D; \
59 reshape to Ix1 first or use a dedicated 1-D helper"
60 ))
61 .with_location(ErrorLocation::new(file!(), line!())),
62 ));
63 }
64
65 // Alignment check: the data pointer must be aligned for B.
66 let ptr = array.as_ptr() as *const B;
67 if (ptr as usize) % std::mem::align_of::<B>() != 0 {
68 return Err(CoreError::ValidationError(
69 ErrorContext::new(format!(
70 "Data pointer is not aligned for the target type (required alignment: {})",
71 std::mem::align_of::<B>()
72 ))
73 .with_location(ErrorLocation::new(file!(), line!())),
74 ));
75 }
76
77 // SAFETY: We have verified:
78 // 1. The array is non-empty (checked by check_not_empty above).
79 // 2. Both element types have the same size, so the stride values remain valid
80 // for B (ndarray's cast asserts size equality internally as well).
81 // 3. The pointer is properly aligned for B (alignment check above).
82 // 4. The caller, by using this `unsafe fn`, guarantees that the byte
83 // representation of every A element is a valid B value.
84 // 5. The lifetime 'a of the returned view is tied to the lifetime of
85 // `array`, so no use-after-free is possible.
86 let raw_view = array.raw_view().cast::<B>();
87 Ok(raw_view.deref_into_view())
88}
89
90/// Create a mutable view of an array with a different element type
91///
92/// # Safety
93///
94/// This function is unsafe because it does not check that the memory layout
95/// of the source type is compatible with the destination type.
96///
97/// # Arguments
98///
99/// * `array` - The array to view
100///
101/// # Returns
102///
103/// A mutable view of the array with elements interpreted as the new type
104pub unsafe fn view_mut_as<A, B, S, D>(
105 array: &mut ArrayBase<S, D>,
106) -> Result<ViewMut<'_, B, D>, CoreError>
107where
108 A: Clone,
109 S: Data<Elem = A> + RawDataMut,
110 D: Dimension,
111{
112 validation::check_not_empty(array)?;
113
114 // Calculate new shape based on type sizes
115 let a_size = std::mem::size_of::<A>();
116 let b_size = std::mem::size_of::<B>();
117
118 if a_size == 0 || b_size == 0 {
119 return Err(CoreError::ValidationError(
120 ErrorContext::new("Cannot reinterpret view of zero-sized type".to_string())
121 .with_location(ErrorLocation::new(file!(), line!())),
122 ));
123 }
124
125 if a_size != b_size {
126 // Reinterpreting between differently-sized element types requires a shape change
127 // that cannot be expressed within the generic `D` dimension parameter; use a
128 // 1-D-specific helper or reshape the array to Ix1 before calling view_mut_as.
129 return Err(CoreError::NotImplementedError(
130 ErrorContext::new(format!(
131 "view_mut_as with differing element sizes ({a_size} vs {b_size}) requires a \
132 shape change not representable in the generic dimension D; \
133 reshape to Ix1 first or use a dedicated 1-D helper"
134 ))
135 .with_location(ErrorLocation::new(file!(), line!())),
136 ));
137 }
138
139 // Alignment check: the data pointer must be aligned for B.
140 let ptr = array.as_ptr() as *const B;
141 if (ptr as usize) % std::mem::align_of::<B>() != 0 {
142 return Err(CoreError::ValidationError(
143 ErrorContext::new(format!(
144 "Data pointer is not aligned for the target type (required alignment: {})",
145 std::mem::align_of::<B>()
146 ))
147 .with_location(ErrorLocation::new(file!(), line!())),
148 ));
149 }
150
151 // SAFETY: We have verified:
152 // 1. The array is non-empty (checked by check_not_empty above).
153 // 2. Both element types have the same size, so strides remain valid for B.
154 // 3. The pointer is properly aligned for B (alignment check above).
155 // 4. The caller, by using this `unsafe fn`, guarantees that the byte
156 // representation of every A element is a valid B value, and that
157 // writing valid B values leaves valid A byte patterns (since sizes
158 // are equal, the allocation remains properly sized).
159 // 5. The lifetime 'a of the returned view is tied to the mutable borrow
160 // of `array`, enforcing exclusive access for the view's lifetime.
161 let raw_view_mut = array.raw_view_mut().cast::<B>();
162 Ok(raw_view_mut.deref_into_view_mut())
163}
164
165/// Create a transposed copy of a 2D array
166///
167/// # Arguments
168///
169/// * `array` - The array to transpose
170///
171/// # Returns
172///
173/// A transposed copy of the array
174#[allow(dead_code)]
175pub fn transpose_view<A, S>(array: &ArrayBase<S, Ix2>) -> Result<Array<A, Ix2>, CoreError>
176where
177 A: Clone,
178 S: Data<Elem = A>,
179{
180 validation::check_not_empty(array)?;
181
182 // Create a transposed owned copy
183 Ok(array.to_owned().t().to_owned())
184}
185
186/// Create a copy of the diagonal of a 2D array
187///
188/// # Arguments
189///
190/// * `array` - The array to view
191///
192/// # Returns
193///
194/// A copy of the diagonal of the array
195#[allow(dead_code)]
196pub fn diagonal_view<A, S>(array: &ArrayBase<S, Ix2>) -> Result<Array<A, Ix1>, CoreError>
197where
198 A: Clone,
199 S: Data<Elem = A>,
200{
201 validation::check_not_empty(array)?;
202 validation::check_square(array)?;
203
204 // Create a diagonal copy
205 Ok(array.diag().to_owned())
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use ndarray::array;
212
213 /// `f32` and `u32` share the same size (4 bytes) and same alignment (4 bytes).
214 /// Viewing an f32 array as u32 reads the raw IEEE-754 bit pattern.
215 #[test]
216 fn test_view_as_same_size_f32_as_u32() {
217 // Positive zero: bit pattern 0x0000_0000
218 let arr = array![0.0_f32, 1.0_f32];
219 // SAFETY: u32 is valid for any 4-byte bit pattern; f32 values we use are
220 // well-defined IEEE-754 values, so reading them as u32 is well-defined.
221 let view = unsafe { view_as::<f32, u32, _, _>(&arr) };
222 assert!(view.is_ok(), "view_as should succeed for same-size types");
223 let view = view.expect("view_as returned Err unexpectedly");
224 // 0.0f32 → 0x0000_0000, 1.0f32 → 0x3F80_0000
225 assert_eq!(view[0], 0x0000_0000_u32);
226 assert_eq!(view[1], 0x3F80_0000_u32);
227 }
228
229 /// `u32` and `i32` share the same size and alignment; reinterpreting should
230 /// succeed and expose the two's-complement representation.
231 #[test]
232 fn test_view_as_u32_as_i32_round_trip() {
233 let arr = array![0_u32, 0xFFFF_FFFF_u32];
234 // SAFETY: every u32 bit pattern is a valid i32 (two's complement).
235 let view = unsafe { view_as::<u32, i32, _, _>(&arr) };
236 assert!(view.is_ok());
237 let view = view.expect("view_as returned Err unexpectedly");
238 assert_eq!(view[0], 0_i32);
239 assert_eq!(view[1], -1_i32);
240 }
241
242 /// Attempting to view a `u8` array as `u32` (different sizes: 1 vs 4) must
243 /// return a `NotImplementedError` because the shape would need to change.
244 #[test]
245 fn test_view_as_different_sizes_returns_error() {
246 let arr = array![0_u8, 1_u8, 2_u8, 3_u8];
247 // SAFETY: irrelevant — we expect an Err before any unsafe memory access.
248 let result = unsafe { view_as::<u8, u32, _, _>(&arr) };
249 assert!(
250 result.is_err(),
251 "view_as with different element sizes must fail"
252 );
253 match result.expect_err("expected an error") {
254 CoreError::NotImplementedError(_) => {}
255 other => panic!("Expected NotImplementedError, got: {other:?}"),
256 }
257 }
258
259 /// `view_mut_as` should succeed for same-size types, and mutations through
260 /// the reinterpreted view must be visible in the original array.
261 #[test]
262 fn test_view_mut_as_same_size_mutates_original() {
263 let mut arr = array![0_u32, 1_u32];
264 {
265 // SAFETY: i32 is valid for any u32 bit pattern we set below.
266 let mut view = unsafe { view_mut_as::<u32, i32, _, _>(&mut arr) }
267 .expect("view_mut_as returned Err unexpectedly");
268 view[0] = -1_i32; // sets bit pattern 0xFFFF_FFFF
269 }
270 // After the view is dropped, inspect via u32 lens
271 assert_eq!(arr[0], 0xFFFF_FFFF_u32);
272 assert_eq!(arr[1], 1_u32); // unchanged
273 }
274
275 /// Attempting to view an empty array must return a `ValidationError`.
276 #[test]
277 fn test_view_as_empty_array_returns_error() {
278 let arr = ndarray::Array1::<f32>::zeros(0);
279 // SAFETY: no memory is accessed; we expect an Err for the empty check.
280 let result = unsafe { view_as::<f32, u32, _, _>(&arr) };
281 assert!(result.is_err(), "view_as on empty array must fail");
282 match result.expect_err("expected an error") {
283 CoreError::ValidationError(_) => {}
284 other => panic!("Expected ValidationError, got: {other:?}"),
285 }
286 }
287}