venus_core/state/
output.rs

1//! Cell output serialization traits.
2//!
3//! ## Serialization Architecture
4//!
5//! Venus uses rkyv for all serialization:
6//!
7//! 1. **Cell data** (stored in `BoxedOutput.bytes`): Serialized with rkyv for
8//!    zero-copy FFI performance when passing data between cells.
9//!
10//! 2. **BoxedOutput container**: Also serialized with rkyv for state persistence
11//!    (saving/loading cached outputs to disk).
12//!
13//! This unified approach provides consistent zero-copy deserialization throughout.
14
15use std::any::TypeId;
16use std::collections::hash_map::DefaultHasher;
17use std::hash::{Hash, Hasher};
18
19use rkyv::{Archive, Deserialize, Serialize};
20
21use crate::error::{Error, Result};
22
23/// Trait for cell outputs that can be serialized and restored.
24///
25/// This trait is automatically implemented for any type that implements
26/// `Serialize + DeserializeOwned + 'static`.
27pub trait CellOutput: Send + Sync + 'static {
28    /// Serialize the output to bytes.
29    fn serialize_output(&self) -> Result<Vec<u8>>;
30
31    /// Get the type hash for schema validation.
32    fn type_hash(&self) -> u64;
33
34    /// Get the type name for debugging.
35    fn type_name(&self) -> &'static str;
36}
37
38/// Blanket implementation for all rkyv-compatible types.
39impl<T> CellOutput for T
40where
41    T: for<'a> Serialize<rkyv::rancor::Strategy<
42            rkyv::ser::Serializer<
43                rkyv::util::AlignedVec,
44                rkyv::ser::allocator::ArenaHandle<'a>,
45                rkyv::ser::sharing::Share,
46            >,
47            rkyv::rancor::Error,
48        >> + Send
49        + Sync
50        + 'static,
51{
52    fn serialize_output(&self) -> Result<Vec<u8>> {
53        rkyv::to_bytes::<rkyv::rancor::Error>(self)
54            .map(|v| v.into_vec())
55            .map_err(|e| Error::Serialization(e.to_string()))
56    }
57
58    fn type_hash(&self) -> u64 {
59        // Known limitation: DefaultHasher is not guaranteed stable across
60        // Rust versions or even runs. This is acceptable for single-session
61        // cache validation within the same process. For cross-session persistence,
62        // a deterministic hasher (FxHash) or structural hash would be needed.
63        let mut hasher = DefaultHasher::new();
64        TypeId::of::<T>().hash(&mut hasher);
65        hasher.finish()
66    }
67
68    fn type_name(&self) -> &'static str {
69        std::any::type_name::<T>()
70    }
71}
72
73/// Deserialize a cell output from bytes.
74///
75/// # Safety
76///
77/// Uses unchecked deserialization for performance. Only safe when reading from
78/// trusted sources (our own cache files and worker processes).
79pub fn deserialize_output<T>(bytes: &[u8]) -> Result<T>
80where
81    T: Archive,
82    T::Archived: Deserialize<T, rkyv::rancor::Strategy<rkyv::de::Pool, rkyv::rancor::Error>>,
83{
84    // SAFETY: We trust data from our own cache and IPC.
85    // Using unchecked deserialization avoids CheckBytes trait complexity.
86    unsafe { rkyv::from_bytes_unchecked::<T, rkyv::rancor::Error>(bytes) }
87        .map_err(|e: rkyv::rancor::Error| Error::Deserialization(e.to_string()))
88}
89
90/// Marker trait for outputs that can use zero-copy deserialization.
91///
92/// Types implementing this trait can potentially be accessed without
93/// full deserialization using rkyv. For now, this is a marker trait
94/// that indicates the type is suitable for high-performance paths.
95pub trait ZeroCopyOutput: CellOutput {}
96
97/// A boxed cell output that can be stored generically.
98#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
99pub struct BoxedOutput {
100    /// Serialized bytes
101    bytes: Vec<u8>,
102
103    /// Type hash for validation
104    type_hash: u64,
105
106    /// Type name for debugging
107    type_name: String,
108
109    /// Human-readable display text (Debug format)
110    display_text: Option<String>,
111}
112
113impl BoxedOutput {
114    /// Create a new boxed output from a CellOutput.
115    pub fn new<T: CellOutput>(value: &T) -> Result<Self> {
116        Ok(Self {
117            bytes: value.serialize_output()?,
118            type_hash: value.type_hash(),
119            type_name: value.type_name().to_string(),
120            display_text: None,
121        })
122    }
123
124    /// Create a boxed output from raw serialized bytes.
125    ///
126    /// Used when loading outputs from FFI calls where type info
127    /// is not available at the Rust level.
128    ///
129    /// **Note**: Type hash is set to 0 (unknown type). This is safe because
130    /// deserialization validates types at runtime. Full type propagation from
131    /// FFI would require codegen changes to embed type metadata in dylibs.
132    pub fn from_raw_bytes(bytes: Vec<u8>) -> Self {
133        Self {
134            bytes,
135            type_hash: 0, // Unknown type
136            type_name: "<ffi>".to_string(),
137            display_text: None,
138        }
139    }
140
141    /// Create a boxed output from raw bytes with display text.
142    ///
143    /// Used when loading outputs from FFI calls that include
144    /// a human-readable representation.
145    pub fn from_raw_bytes_with_display(bytes: Vec<u8>, display: String) -> Self {
146        Self {
147            bytes,
148            type_hash: 0, // Unknown type
149            type_name: "<ffi>".to_string(),
150            display_text: Some(display),
151        }
152    }
153
154    /// Create a boxed output from raw bytes with known type info.
155    ///
156    /// Used when restoring outputs from Salsa cache where type
157    /// information was preserved.
158    pub fn from_raw_with_type(bytes: Vec<u8>, type_hash: u64, type_name: String) -> Self {
159        Self {
160            bytes,
161            type_hash,
162            type_name,
163            display_text: None,
164        }
165    }
166
167    /// Get the serialized bytes.
168    pub fn bytes(&self) -> &[u8] {
169        &self.bytes
170    }
171
172    /// Get the type hash.
173    pub fn type_hash(&self) -> u64 {
174        self.type_hash
175    }
176
177    /// Get the type name.
178    pub fn type_name(&self) -> &str {
179        &self.type_name
180    }
181
182    /// Get the display text (Debug format) if available.
183    pub fn display_text(&self) -> Option<&str> {
184        self.display_text.as_deref()
185    }
186
187    /// Deserialize to a specific type.
188    ///
189    /// Returns an error if the type hash doesn't match.
190    pub fn deserialize<T>(&self) -> Result<T>
191    where
192        T: CellOutput + Archive,
193        T::Archived: Deserialize<T, rkyv::rancor::Strategy<rkyv::de::Pool, rkyv::rancor::Error>>,
194    {
195        // Verify type hash (see type_hash() for hash stability notes)
196        let expected_hash = {
197            let mut hasher = DefaultHasher::new();
198            std::any::TypeId::of::<T>().hash(&mut hasher);
199            hasher.finish()
200        };
201
202        if self.type_hash != expected_hash {
203            return Err(Error::SchemaEvolution(format!(
204                "Type mismatch: stored {} (hash {:x}), requested {} (hash {:x})",
205                self.type_name,
206                self.type_hash,
207                std::any::type_name::<T>(),
208                expected_hash
209            )));
210        }
211
212        deserialize_output(&self.bytes)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[derive(Debug, Clone, PartialEq, Archive, Serialize, Deserialize)]
221    struct TestOutput {
222        value: i32,
223        name: String,
224    }
225
226    #[test]
227    fn test_cell_output_serialize() {
228        let output = TestOutput {
229            value: 42,
230            name: "test".to_string(),
231        };
232
233        let bytes = output.serialize_output().unwrap();
234        assert!(!bytes.is_empty());
235
236        let restored: TestOutput = deserialize_output(&bytes).unwrap();
237        assert_eq!(output, restored);
238    }
239
240    #[test]
241    fn test_type_hash_consistency() {
242        let output1 = TestOutput {
243            value: 1,
244            name: "a".to_string(),
245        };
246        let output2 = TestOutput {
247            value: 2,
248            name: "b".to_string(),
249        };
250
251        // Same type should have same hash
252        assert_eq!(output1.type_hash(), output2.type_hash());
253
254        // Different type should have different hash
255        let other: i32 = 42;
256        assert_ne!(output1.type_hash(), other.type_hash());
257    }
258
259    #[test]
260    fn test_boxed_output() {
261        let output = TestOutput {
262            value: 42,
263            name: "test".to_string(),
264        };
265
266        let boxed = BoxedOutput::new(&output).unwrap();
267        assert!(boxed.type_name().contains("TestOutput"));
268
269        let restored: TestOutput = boxed.deserialize().unwrap();
270        assert_eq!(output, restored);
271    }
272
273    #[test]
274    fn test_boxed_output_type_mismatch() {
275        let output = TestOutput {
276            value: 42,
277            name: "test".to_string(),
278        };
279
280        let boxed = BoxedOutput::new(&output).unwrap();
281
282        // Try to deserialize as wrong type
283        let result: Result<i32> = boxed.deserialize();
284        assert!(result.is_err());
285        assert!(result.unwrap_err().to_string().contains("Type mismatch"));
286    }
287
288    #[test]
289    fn test_primitive_outputs() {
290        // Test that primitives work as cell outputs
291        let int_val: i64 = 12345;
292        let bytes = int_val.serialize_output().unwrap();
293        let restored: i64 = deserialize_output(&bytes).unwrap();
294        assert_eq!(int_val, restored);
295
296        let string_val = "hello world".to_string();
297        let bytes = string_val.serialize_output().unwrap();
298        let restored: String = deserialize_output(&bytes).unwrap();
299        assert_eq!(string_val, restored);
300
301        let vec_val = vec![1, 2, 3, 4, 5];
302        let bytes = vec_val.serialize_output().unwrap();
303        let restored: Vec<i32> = deserialize_output(&bytes).unwrap();
304        assert_eq!(vec_val, restored);
305    }
306}