numrs/array_view.rs
1//! ArrayView - Zero-copy view into typed data for FFI operations
2//!
3//! Este struct resuelve el problema de performance en FFI:
4//! - El caller mantiene ownership de los datos en C/Python/JS
5//! - ArrayView contiene solo referencias (slices) - NO copia datos
6//! - Compatible con dispatch system existente
7//!
8//! # Arquitectura
9//! Para FFI, usamos un patrón de "view handle":
10//! 1. C API recibe void* ptr del caller (datos en memoria C)
11//! 2. Rust crea ArrayView con slice::from_raw_parts (sin to_vec!)
12//! 3. ArrayView se guarda en Box y retorna opaque pointer al caller
13//! 4. Caller usa ese handle para múltiples operaciones
14//! 5. Caller destruye el handle cuando termina
15
16use crate::array::DType;
17
18/// View into typed array data - wraps borrowed slices (no copies!)
19///
20/// # Diseño
21/// Para evitar to_vec() completamente, ArrayView contiene referencias
22/// con lifetime 'static (seguro porque el caller garantiza que los datos
23/// viven mientras el view existe - via Box en C API).
24///
25/// # Uso en FFI
26/// ```ignore
27/// // C tiene: float* data_a, float* data_b (10000 elementos cada uno)
28///
29/// // Crear views (sin to_vec! - solo crea slice):
30/// let view_a = unsafe { ArrayView::from_ptr_f32(data_a, 10000) };
31/// let view_b = unsafe { ArrayView::from_ptr_f32(data_b, 10000) };
32///
33/// // Múltiples operaciones - todas zero-copy:
34/// ops_inplace::elementwise_view(&view_a, &view_b, out, Add)?; // ~23μs
35/// ops_inplace::elementwise_view(&view_a, &view_b, out, Mul)?; // ~23μs
36/// ops_inplace::elementwise_view(&view_a, &view_b, out, Sub)?; // ~23μs
37/// // Sin to_vec(), cada operación corre a velocidad nativa!
38/// ```
39#[derive(Debug, Clone)]
40pub enum ArrayView {
41 F32(&'static [f32]),
42 F64(&'static [f64]),
43 I32(&'static [i32]),
44 I64(&'static [i64]),
45 U8(&'static [u8]),
46}
47
48impl ArrayView {
49 /// Create from f32 slice (zero-copy via raw pointer)
50 ///
51 /// # Safety
52 /// Caller must ensure data outlives the ArrayView. For FFI, this is safe
53 /// because ArrayView is boxed and C API controls lifetime.
54 pub fn from_slice_f32(data: &[f32]) -> Self {
55 unsafe { Self::from_ptr_f32(data.as_ptr(), data.len()) }
56 }
57
58 /// Create from f64 slice (zero-copy via raw pointer)
59 pub fn from_slice_f64(data: &[f64]) -> Self {
60 unsafe { Self::from_ptr_f64(data.as_ptr(), data.len()) }
61 }
62
63 /// Create from i32 slice (zero-copy via raw pointer)
64 pub fn from_slice_i32(data: &[i32]) -> Self {
65 unsafe { Self::from_ptr_i32(data.as_ptr(), data.len()) }
66 }
67
68 /// Create from raw pointer (unsafe - for FFI)
69 ///
70 /// # Safety
71 /// - `ptr` must be valid for `len` elements
72 /// - Data must outlive the ArrayView (caller's responsibility in C API via Box)
73 /// - 'static lifetime is safe because C API holds Box<ArrayView> until destroy()
74 pub unsafe fn from_ptr_f32(ptr: *const f32, len: usize) -> Self {
75 ArrayView::F32(std::slice::from_raw_parts(ptr, len))
76 }
77
78 /// Create from raw pointer (unsafe - for FFI)
79 pub unsafe fn from_ptr_f64(ptr: *const f64, len: usize) -> Self {
80 ArrayView::F64(std::slice::from_raw_parts(ptr, len))
81 }
82
83 /// Create from raw pointer (unsafe - for FFI)
84 pub unsafe fn from_ptr_i32(ptr: *const i32, len: usize) -> Self {
85 ArrayView::I32(std::slice::from_raw_parts(ptr, len))
86 }
87
88 /// Get dtype
89 pub fn dtype(&self) -> DType {
90 match self {
91 ArrayView::F32(_) => DType::F32,
92 ArrayView::F64(_) => DType::F64,
93 ArrayView::I32(_) => DType::I32,
94 ArrayView::I64(_) => DType::I32, // Fallback to I32 for now
95 ArrayView::U8(_) => DType::U8,
96 }
97 }
98
99 /// Get length
100 pub fn len(&self) -> usize {
101 match self {
102 ArrayView::F32(v) => v.len(),
103 ArrayView::F64(v) => v.len(),
104 ArrayView::I32(v) => v.len(),
105 ArrayView::I64(v) => v.len(),
106 ArrayView::U8(v) => v.len(),
107 }
108 }
109
110 /// Check if empty
111 pub fn is_empty(&self) -> bool {
112 self.len() == 0
113 }
114
115 /// Get as f32 slice (zero-copy)
116 pub fn as_f32(&self) -> Option<&[f32]> {
117 match self {
118 ArrayView::F32(v) => Some(v),
119 _ => None,
120 }
121 }
122
123 /// Get as f64 slice (zero-copy)
124 pub fn as_f64(&self) -> Option<&[f64]> {
125 match self {
126 ArrayView::F64(v) => Some(v),
127 _ => None,
128 }
129 }
130
131 /// Get as i32 slice (zero-copy)
132 pub fn as_i32(&self) -> Option<&[i32]> {
133 match self {
134 ArrayView::I32(v) => Some(v),
135 _ => None,
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_array_view_f32() {
146 // Leak data to get 'static lifetime (ok for tests)
147 let data: &'static [f32] = Box::leak(vec![1.0f32, 2.0, 3.0].into_boxed_slice());
148 let view = ArrayView::from_slice_f32(data);
149
150 assert_eq!(view.len(), 3);
151 assert_eq!(view.dtype(), DType::F32);
152 assert_eq!(view.as_f32().unwrap(), &[1.0, 2.0, 3.0]);
153 }
154
155 #[test]
156 fn test_zero_copy_reference() {
157 let data: &'static [f32] = Box::leak(vec![1.0f32; 10000].into_boxed_slice());
158 let view = ArrayView::from_slice_f32(data);
159
160 // Multiple calls to as_f32() return SAME pointer - true zero-copy
161 let slice1 = view.as_f32().unwrap();
162 let slice2 = view.as_f32().unwrap();
163
164 assert_eq!(slice1.as_ptr(), slice2.as_ptr());
165 assert_eq!(slice1.as_ptr(), data.as_ptr()); // Points to original data!
166 }
167
168 #[test]
169 fn test_from_ptr_zero_copy() {
170 let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
171 let ptr = data.as_ptr();
172 let len = data.len();
173
174 unsafe {
175 let view = ArrayView::from_ptr_f32(ptr, len);
176
177 // View apunta directamente a data - no hay to_vec()
178 assert_eq!(view.as_f32().unwrap().as_ptr(), ptr);
179 assert_eq!(view.len(), len);
180 }
181
182 // Keep data alive
183 drop(data);
184 }
185}